(roughly in order of importance / new-ness)
- set
keytovalue:(=) (key, value) => print(key, value) - set
keytokey + value:(+=) (key, value) => ... - get
key:() key => ...
- Contains itself (recursively), just like already implemented for functions:
0.value == 0"hey".value == "hey"true.value == trueprint.value == print
- Field name:
x.value: same for all types. Possibly add this for all values (including blocks/objects) as.self? Any use case?- For assigning to variable keys, which would otherwise no longer be
possible with the new index==call syntax:
{ x = 10; self(x) = "ten!" }
- For assigning to variable keys, which would otherwise no longer be
possible with the new index==call syntax:
x.Number: possibility for getter function to convert to this type- example: a boolean value can have
.Bool,.Numberand.String - problem: conversion should be a function (at least lazily computed)
- example: a boolean value can have
x.(Number): same as.Numberbut with the type as key instead of the namex."()"for functions: as already implemented for the call operator(). Does not generalise to the other basic value types though
-
Before:
a = 10 -
After:
a = (10 + 20) -
Alternatively, only allow single newline within expression
-
Before:
a = 10 + 20 -
After:
a = 10 + 20 -
Need to work out how (line) comments interact with this. Probably easiest to still count an empty line with line comment as two newlines.
-
- Expression
+is just a function(l, r) => l + r - Probably will need to enclose in parentheses most of the time, to prevent it being parsed as an operator application.
- Can be implemented as method on symbols?
- kind of defeats the purpose of symbols as inert, atomic names
(1, 2) + (4, 8) == (5, 10)a, b = (1, 2) + (4, 8)===a, b = 1 + 4, 2 + 8a, b += 4, 8===a, b = a + 4, b + 8===a, b = (a, b) + (4, 8)
Example with unary operator:
even x => x % 2 == 0
odd = !even -- odd x => !even(x)
print(odd(7)) --> true
-- Filtering only odd values:
stream.filter(x => !even(x))
stream.filter(!even)
Example with binary operator:
double x => x * 2
succ x => x + 1
f = double - succ -- f x => double x - succ x
print(f 10) --> 9
(.f) == (x => x.f),(.f)(x) == x.f()
Example:
(.isEven)(7) == 7.isEven()
stream.filter(x => x.isEven())
stream.filter(.isEven)
Combined with auto-defined operators for functions:
stream.filter(x => !x.isEven())
stream.filter(!.isEven)
How to work with binary functions, without partial application?
stream.map(x => x + 10)stream.map(.+ 10)would not work (as that would bestream.map(10.+()))
- Operators for indexing:
a.+,a.++,a.//(equivalent toa."//") - Operators for defining:
{ // (l, r) => ... }(equivalent to{ "//" = (l, r) => ... }) - Keywords for indexing:
a.if,a.var,a.return(equivalent toa."return") - Keywords for defining:
{ if = ... }(equivalent to{ "if" = ... })- Probably not, this may be hard to parse for computer and human.
{ const var = 10 }? (constant calledvar){ var x => ... }??? (function calledvartaking parameterx)
- Probably not, this may be hard to parse for computer and human.
- Sources: groups, return (-> function calls), yield/break
- Destinations: function arguments, assignments, list contents
- prefix
!, like Icon? - prefix
$, as a sort ofS?
- Ability to (re-)define receiver for bound methods
a.b::c.dcreates functionc.dwitha.bas receiver (this)- Call like
a.b::c.d(...)
- Call like
- Useful for "extending" standard libraries
- Example: suppose I want to add a
mapfunction to objects:obj::ObjExtensions.map(x => x + 1)orobj::map(x => x + 1)instead ofObjExtensions.map(obj, x => x + 1)
- Example: suppose I want to add a
- https://github.com/tc39/proposal-bind-operator
- "global" functions and functions in current scope get current block as
this - or, they get the function itself as
this - or, the current
thisis propagated
- Words (Lua):
if condition then body,for var in val do body,for var in val do { body }- cons: long/wordy, 2 extra keywords, different keywords for
ifandfor/while
- cons: long/wordy, 2 extra keywords, different keywords for
- Colon (Python):
if condition: body,for var in val: body,for var in val: { body }- cons: looks like python so makes you forget
{},:and{}looks superfluous
- cons: looks like python so makes you forget
- Comma (Jammy):
if condition, body,for var in val, body,for var in val, { body }- cons: confusing in lists,
,and{}looks superfluous
- cons: confusing in lists,
- Parentheses (C):
if (condition) body,for (var in val) body,for (var in val) { body }- cons: not spaceous / many punctuation chars, parentheses suddenly part of syntax
-
Also for
while? Not forfor -
Definition only valid in scope of if-branch
if var x = fn(): { print x } else { print "something else" }
-
Function assignment shorthand (as opposed to definition)
- Now,
a.f() => 42is no longer valid, onlya.f := () => 42
- Now,
-
Maybe allow same-level shadowing?
- could be hard to implement or confusing to use
-
Warning on unused variables, to prevent errors when accidentally shadowing instead of mutating (i.e. using
=instead of:=)var x = 0 if x > 10: { x = 42 } -- x is still 0 -
Values should also be const by default? Otherwise this is weird:
obj = { x = 10 } obj.y = 20 -- possible obj.x = 30 -- not possible, x is const -
Shorthand notation for mutable blocks: (
aandbarevarhere)obj = var { a = 10 b = 20 }
- Value that cannot change, as opposed to variable that cannot be reassigned
- Immutable value means that every containing field is immutable
and that no new fields can be added
- override
_Setmethod to prevent adding fields
- override
- Potential problem with native (C-side) code: should be impossible to change value/pointer via C
- Primitives like Bool, Int, Float, String are immutable by default
- Options for constant variable
aholding immutable value{}:- add
readonly(C#) /sealed(C#) /immutable/immutmodifier:
const immutable a = {}orconst readonly a = {} - allow using
constfor values: (confusing)
const a = const {} - extend
constto also apply to values: (confusing for JS/Java users)
const a = {} - use
freezefunction: (like JS)
const a = freeze {}
- add
- Enforce for thread-safety: can only share immutable values
- No reassignment by default? -> only use
constfor immutability andmut/varfor reassignable - Probably: (like JS) no syntax for immutable values, maybe freeze function. Immutability checking (for threads) needs to be a function as well
==/!=for equality,===/!==for identity- pro:
===looks like== - like Kotlin, JS also has these operators (with different semantics though)
- pro:
==/!=for equality,eq/neqfor identity- pro: clearer distinction between overloadable/non-overloadable operators (all keyword operators are non-overloadable)
- for negated identity:
neq,neor!eq(Kotlin style)?
.eq/.neqfunction for equality,==/!=for identity- pro: simple, just a function call (no syntax needed!)
- con:
==should be overloadable (it is a 2-character operator)
print "hello"--> helloprint("hello".inspect())--> "hello"
- What to do when caller explicitly passes nil?
- python will use nil/None, not default value
- having separate nil and nothing can help
- Following C# valid-ness, valid when one of:
- First all positional parameters, then all named parameters
- Named parameters in correct place?
- problem: group is not a data structure
- possible solution: make group (ephemeral) data structure (introduce "tuples")
- Making it
constdoes not fix: shadowing - Linter cannot catch everything:
a = nil; (a) = 42will assign to nil - Also
trueandfalse?
- try as method on functions:
fn.try(args)like Lua's pcall - http://joeduffyblog.com/2016/02/07/the-error-model/
- http://lua-users.org/wiki/FinalizedExceptions
- Errors like Lua for bugs / unrecoverable programmer errors
- Return values for recoverable errors
- On success:
return nil, value, ... - On failure:
return Error("description") err, val1, val2 = fn()
- On success:
- Error value is falsy, possible idiom using
elseororprint(fn() else "failure")x = fn() else "default value"
- match function, function parameter overloading, match operator
~~? - match against values, number of values, lists, blocks, types, (destructuring), ...
- default implementation of match operator
~~is equality,withinfor ranges, ...
- default implementation of match operator
- problem: now need to execute match operator functions to decide which function to call
- solution: no match operator, so not overloadable
- problem:
trueandfalseare variables, so(true) -> ...always matches - value:
42 -> ... - variable:
x -> ... - multiple variables:
(x, y) -> ... - all variables:
(...x) -> ... - var with rest:
(x, ...y, z) -> ... - variable as value:
\(x) -> ... - lists:
[x, y] -> ... - blocks:
{ x, y } -> ... - blocks with values
{ x = 10 } -> ... - blocks with renaming?
{ x = a } -> ...(don't know if this or swapped:a = x) - types?
(x: String) -> ... - composite structures:
[{x1, y1}, {x2, y2}] -> ...,[10, x] -> ...,[\(x)] -> ...
Specify yielding coroutine or yield location? To allow using coroutine yielding for both async stuff and more local stream stuff.
- Option 1: specify coroutine to yield from
- code running that coroutine will resume
- Option 2: specify coroutine to yield to
- that coroutine will resume
- Different from symmetric coroutines because stack stays intact? Yield point needs to be on the call stack.
- Similar to Wren's
Fiber.transferfunction: https://wren.io/concurrency.html#transferring-control- different because Wren allows transfer between any coroutine
- Transparent coroutines? (very vague idea)
-
Plain call is run to completion, special syntax to instantiate coroutine
-
This program prints "before a 2 4 after"
f() => { print "a"; b = yield "b"; print b; c = yield "c"; return c + 1 } g() => { print "before"; print(f()); print "after" } run g() [ "b" => continue 2 -- `continue` means re-do as a loop "c" => continue 3 ]
Example using Lua coroutine function names:
-- "Scheduler" code
main = Coroutine.running()
-- User code
co = Coroutine(() => {
generator = Coroutine(() => {
yield(10) --> yields to for loop
-- Option 1 equivalent:
Coroutine.running().yield(10)
-- Other option 1 equivalent:
generator.yield(10)
-- Option 2 equivalent:
co.yield(10)
-- Yields to main coroutine, "through" for loop coroutine
co.yield("sleep") -- option 1
main.yield("sleep") -- option 2
yield(20) --> yields to for loop
})
-- for loop resumes coroutine for each element
for x in generator: print x
})
-- "Scheduler" code
val = co.resume() --> prints 10
-- val == "sleep"
co.resume() --> prints 20
- Important to allow only one vararg
- Like ipv6 shorthand notation :)
- only:
f(...rest) => () - at end:
f(a, ...rest) => () - at start:
f(...rest, a) => () - between:
f(a, ...rest, b) => ()
camelCase(Lua, Java, Kotlin): requires typing shift
defineProtoNativeFn()lowercase(Lua): requires no extra typing, but can become harder to read
defineprotonativefn()snake_case(Rust, C, Python): requires typing both shift and-
define_proto_native_fn()kebab-case(Lisp): only requires typing-, require spaces around infix operators, vscode does not recognise as single word
define-proto-native-fn()
-
https://discord.com/channels/530598289813536771/530604512017121290/954101398243520644
-
https://github.com/airstruck/knife/blob/master/readme/chain.md
-
https://idris2.readthedocs.io/en/latest/tutorial/interfaces.html#notation
-
Functor:
(Promise<A>, A -> B) -> Promise<B> -
Monad:
(Promise<A>, A -> Promise<B>) -> Promise<B> -
For things like Javascript's Promise (which is not always a monad) or Maybe With the example from MDN:
fetch("https://github.com") .then(response => response.json()) .then(data => print(data))With pipe operator:
fetch("https://github.com") |> response => response.json() |> data => print(data)With pipe operator and holes:
fetch("https://github.com") |> _.json() |> print(_)In callback style:
fetch("https://github.com", response => { response.json(data => { print(data) }) })With do-notation:
response <- fetch("https://github.com") data <- response.json() print(data)With use-notation:
use response <- fetch("https://github.com") data = response.json() print(data)Pro of use-notation: can be used without value (example from Gleam website)
use <- defer(() => print "Goodbye") print "Hello"
Like JS obj.addEventListener(eventName, callback)
- Register callback:
obj.event.map(callback) - Wait for single event:
obj.event.then(callback)- or with do-notation:
x <- obj.event
- or with do-notation:
Problem: streams are pull-based, events should be push-based?
How to "unbind" callback from event source?
Turn a.x(); a.y(); a.z() into with a: { x(); y(); z() }
a = { x = 10 }
b = { ...a; y = 20 }
b == { x = 10; y = 20 }
- Don't like tuples; what do you need them for?, less generic because hard-coded data structure
- Immutable -> can be stored on the stack -> more performant?
- Evaluating semi-tuple still results in content (otherwise can't do
(1+2)*3)- Therefore, these are not normal tuples
- Cannot compose:
((a, b), (c, d))is(a, b, c, d)isa, b, c, d
- Supports named fields:
(x = 10, y = 20)
y, x from (x = 10, y = 20)
return (x = 10, y = 20)
fn (x = 10, y = 20)
- as definition:
a, b, c, d, e = (4, a = 1, b = 2, 5, x = 3) - as function call:
f(a, b, c, d, e) => ...; f(4, a = 1, 5, b = 2, x = 3)
| Match method | a |
b |
c |
d |
e |
note |
|---|---|---|---|---|---|---|
| Ignore names | 4 |
1 |
2 |
5 |
3 |
👎 why bother with names in the first place? |
| Override named with positional | 4 |
5 |
(nil) | (nil) | (nil) | 🤔 could work (probably not) |
| Override positional with named | 1 |
2 |
(nil) | (nil) | (nil) | 👍 promising, prioritises explicit-ness |
| Override earlier with later | 1 |
5 |
(nil) | (nil) | (nil) | 👍 promising, reflects assignment order |
| Named first, append positional | 1 |
2 |
4 |
5 |
(nil) | 👎 confusing, named args slide over |
| Error on inconsistency | - | - | - | - | - | 👎 only for statically typed languages |
- Usage: empty indices in lists while iterating (atm iteration stops at nil)
- otherwise, iteration could skip empty (not present!) indices, use iteration with range to not-skip
- Usage: removing variables from objects (setting to nil does not remove atm)
- otherwise, remove when set to nil
- One value instance of singleton class
- set to this value: variable has this value
- behaves just like value, is kept in lists
[10, nil, 20].length == 3,for x in () => { yield 10; yield nil; yield 20 }loops 3 times
- One keyword with no corresponding value
- set to this keyword: remove reference
- behave like nothing was present, removed in lists, stops iteration
[10, nil, 20] == [10, 20],for x in () => { yield 10; yield nil; yield 20 }loops once
- Attempting to use keyword kind as value yields value kind? (keyword kind == value kind is true)
- Otherwise, using
==on keyword kind results in error asa == bgets transformed intoa."=="(b)and keyword kind does not have==field
- Otherwise, using
- How to check if a variable is nil or nothing? Is it really necessary?
- Using reference identity,
nothing eq nilisfalse
- Using reference identity,
- with explicit return:
() => { ...; return } - with
dofunction:() => do { ... }
do(_) => nil
- Arithmetic operators for lists are defined as a map on that list:
a + b == a.map((x,y) => x + y) - Need other operator for concatenation:
++like haskell,..like lua is already taken by the range operator
- Ambiguous, what is
x => body?- anonymous function with single parameter
- getter function named
x
- maybe as a macro that marks the block as parallel-accessible?
- only allowed to access parallel-accessible Blocks of other threads, will auto-lock
- problem: needs "ownership": of which thread is the Block
- maybe better to have one type of container for parallel access
- automatically make block parallel-accessible somehow
-
Lazy streams: important! so pull, not push
-
With operator
||>or maybe replace operator|> -
Using coroutines, yielding and iterators
evenInts = [] i = 0 for x in generateIntegers: { i = i+1 if i >= 10: break if x % 2 == 0: evenInts.push(x) }can then be rewritten to
evenInts = generateIntegers ||> filter(x => x % 2 == 0) ||> limit(10) ||> reduce((a, x) => a.push(x), []) -
other example:
first10words = stdin ||> groupByWords ||> limit(10) ||> [] -
Operators to create / extend streams (= coroutines?):
- Like Icon (co-expressions), which has
"a" | "an" | "the" - Stream concatenation (+ appending?):
s ++ "the"(forsa stream)
- Like Icon (co-expressions), which has
- keys are sorted by their addition time
- what happens when deleting (setting to nil) and re-inserting a key?
- Stored at prototype-defined memory indices instead of in hash-table
- https://v8.dev/blog/fast-properties
- Like Python's slots (explicit) or automatically-defined (implicit)
{a, b}is{a = a; b = b}- https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Object_initializer#property_definitions
- Use comma or semicolon? Probably semicolon
- Clashes with expression-as-statement
- Solution: only allow for identifiers as other types are not really interesting anyway
{ :a; :b },{ const :a, :b },{ var :a, :b }- con: can no longer use
:for symbols - con: may not be clear that this defines
aandb
- con: can no longer use
{ =a; =b },{ const =a, =b },{ var =a, =b }
a.b.c = 10isa = { b = { c = 10 } }- Especially useful for configurating with default values
- Possible confusion with assignment
a.b.c := 10- Though this could also be used for immutable update:
aNew = a.with { b.c = 10 }.withneeds to be deep for this to work (needs to copy over other values ofb)
- Though this could also be used for immutable update:
- Analogous pattern:
{ a.b } = { a = { b = 10 } }; b == 10 - Wild pattern idea:
{ a.{b.c; d} } = { a = { b.c = 10; d = 20 } }; c == 10 && d == 20
- New style:
var a = if x > 10: "greater" elseif x == 10: "equal" else: "smaller" - In Lua style:
var a = x > 10 and "greater" or (x == 10 and "equal" or "smaller") breakwith value, could also allow for multi-level break? i.e.break break
value = for x in [1,2,3,4,5]: if x % 2 && x % 3: break x- https://news.ycombinator.com/item?id=8827843
- https://news.ycombinator.com/item?id=8828230
- Maybe make assignment an expression
- Pro: terse
a = b = c(needs to be right-associative for this:a = (b = c)) - Con: cryptic "code-golf" things
- Con: possible mistakes like
while x = 1
- Pro: terse
- structural
- operations on types? for more safe typing (algebraic data type like?)
- or, and, not
- constraints on values, for even more safe typing
- natural numbers, hex numbers, numbers 0..1, ascii values A..Z
- microsoft/TypeScript#15480
- maybe better to keep this to documentation or assert
- Structural typing is problem for native types
- Int should be subtype of Float (any int is also a float)
- Float should not be sybtype of Int, but structure might not differentiate
- solution: probably methods will differentiate
- Solution: only use structural subtyping for non-native / non-external types?
- Generic types
- Types == values, using standard operators
|,->- Problem: needs multiple-value operators for
(Int, Int) -> Int
- Problem: needs multiple-value operators for
- Difference:
- traits can define preconditions (methods that need to be present)
- mixins make linear chain (a -> B -> C), traits are flattened (A -> (B, C))
- Automatic? i.e. can use
!=,>automatically on class defining only==and<= - https://blog.10pines.com/2014/10/14/mixins-or-traits/
- https://stackoverflow.com/a/23124968
- Used for:
- adding metadata like documentation, deprecation (removed at compile-time, used by Java, Wren and Rust annotations/attributes)
- adding functionality to a value like sealing, adding methods (used by Typescript and Python decorators)
- Java's annotations: https://docs.oracle.com/javase/tutorial/java/annotations/basics.html
- Wren's attributes: https://wren.io/classes.html#attributes
- Rust's attributes: https://doc.rust-lang.org/reference/attributes.html
- Typescript's decorators: https://www.typescriptlang.org/docs/handbook/decorators.html
- Python's decorators: https://peps.python.org/pep-0318/
- Boolean should be an enum with 2 values? true and false are the only 2 instances of Bool
- Maybe like Java?
- Enum instance (true, false, colours) is const instance with no values, enum "class" itself should be const as well (but do not enforce)
🙂 comment 🙃👉 comment 👈❗ comment