Files
beginner-friendly-lang/spec.md

460 lines
11 KiB
Markdown

## Arithmetic
```
1.2 * 4 + 1
# result: 5.8
```
## Few types to learn
Most important ones:
- `Num`: big-decimal
- `List[T]`: (linked-) list of `T`
- `Char`: unicode codepoint
- `Unit`: nothing
- `i -> o`: function
- `template a, b: t`
Advanced (uncommon) types:
- `Int8`, `Int16`, ...
- `Uint8`, ...
- `Flt32`, `Flt64`
## Functions
```
# the type of msg is: List[Char] -> List[Char]
def msg(username: List[Char]) -> List[Char] {
"Hello, " ++ username ++ "!" # last expr is returned
}
# the type of main is: Unit -> IO[Unit]
def main() -> IO[Unit] {
print("hi")
}
```
## Anonymus functions
```
The type of List.map is List[t] -> (t -> t) -> List[t]
List.map(li, x:Num -> x * 2)
```
## Simple, forward type-inference
```
def zero () -> Flt32 {
3.1 # error: got Num, but expected Flt32
}
```
## Partial function applications
```
# type of add is Num -> Num -> Num
def add(a: Num, b: Num) -> Num {
a + b
}
let a = add(1) # type of a is Num -> Num
let b = a(2) # type of b is Num
# b is 3
```
## Bindings
```
let name = "Max"
let passw = "1234"
```
## no confusing function or operator overloading
all operators:
- `Num + Num` (has overloads for fixed width number types)
- `Num - Num` (has overloads for fixed width number types)
- `Num * Num` (has overloads for fixed width number types)
- `Num / Num` (has overloads for fixed width number types)
- `Num ^ Num`: raise to the power (has overloads for fixed width number types)
- `List[t] ++ List[t]`: list concatenation
- `value :: t` (explicitly specify type of value, useful for down-casting structs, or just code readability; does not perform casting)
- `list[index]`
- `a => b`: lens compose
## non-nominal struct types
```
# `type` creates a non-distinct type alias
type User = { name: List[Char] }
type DbUser = { name: List[Char], pass: List[Char] }
def example4(u: User) -> List[Char] {
u:name # colon is used to access fields
}
def example(u: User) -> DbUser {
u with pass: "1234"
# has type { name: List[Char], pass: List[Char] }
}
def example2() -> {name: List[Char], pass: List[Char]} {
{name:"abc", pass:"123"}
}
def example3() -> User {
example2() # {name:.., pass:...} can automatically decay to {name:...}
}
```
## (tagged) union types
```
type Option[t] =
'Err # If no type specified after tag, defaults to Unit
| 'Some t
# the tags of unions are weakly attached to the types, but won't decay unless they have to
def example(n: Num) -> Num {
let x = 'MyTag n # type of x is 'MyTag Num
x # tag gets removed because target type is Num
}
def example2(n: Num) -> Option[Num] {
'Some n
}
def example3-invalid() -> Option[Num] {
Unit # error: can't convert type `Unit` into type `'Err Unit | 'Some Num`
# Either label the expression with 'Err,
# or change the return type to Option[Unit], and label the expression with 'Some
}
def exampe4() -> Option[Num] {
'Err Unit
# type of this expression is: `'Err Unit`
# enums can automatically cast, if all the cases from the source enum also exists in the target enum,
# which they do here: `'Err Unit` is a case in `'Err Unit | Num`
}
def example5-error() -> Option[Num] {
let x = ( 'Err Unit ) :: Option[Unit]
x
# error: can't convert type `'Err Unit | 'Some Unit` into type `'Err Unit | 'Some Num`
# The case `'Some Unit` does not exist in the target `'Err Unit | 'Some Num`
}
def example6-error() -> Option[Unit] {
let x = 'Error Unit
x
# in this case, the enum tag does not decay, like in `example`,
# because we are casting to an enum
# error: can't convert type `'Error Unit` into type `'Err Unit | 'Some Num``
# 1st possible solution: manually cast to just `Unit` (via `expr :: Unit`), so that it can convert to the second case of the target
# 2nd possible solution: pattern match against the enum, to rename the tag from 'Error to 'Err
}
```
### pattern 1: labelled arguments
```
def [t] List.remove_prefix(prefix: List[t], list: 'from List[t])
List.remove_prefix([1,2], 'from [1,2,3,4])
```
## automatic return types
```
def add(a: Num, b: Num) -> _ {
a + b
}
```
## templated generics
```
# Type of add is: template a, b: a -> b -> _
def [a,b] add(a: a, b: b) -> _ {
a + b
}
add(1,2)
add(1)(2) # error: partial function application of templated functions not allowed
add(1,"2") # error: in template expansion of add[Num,List[Char]]: No definition for `Num + List[Char]`
```
## pattern matching
```
type Option[t] = 'None | 'Some t
def [t] Match.`a++b`(
# matching against this value
value: List[t],
# left hand side of operator
l: List[t],
# right hand side of operator
r: MatchUtil.Var[List[t]]
) -> Option[{ r: List[t] }] {
match List.remove_prefix(l, 'from value) {
'Some rem -> 'Some { r: rem }
'None -> 'None
}
}
```
then you can do:
```
type Token = 'Public | 'Private | 'Err
def example(li: List[Char]) -> {t:Token,rem:List[Char]} {
match li {
"public" ++ rem -> {t: 'Public Unit, rem:rem}
"private" ++ rem -> {t: 'Private Unit, rem:rem}
_ -> {t: 'Err Unit, rem: li}
}
}
```
## recursive data types
```
type List[t] = 'End | 'Cons {head:t, tail:List[t]}
# now you might notice an issue with this
# `type` defines non-distinct type alisases
# so what is the type of this...
# Introducing: type self references
# the above example is the same as this:
type List[t] = &a ('End | 'Cons {head:t, tail:a})
# example 2:
# a List[List[t]] is just:
&b ('End | 'Cons {head: &a ('End | 'Cons {head:t, tail:a}), tail: b})
```
Infinitely sized types are not allowed:
```
&a {x:Num, y:a}
```
However, infinite types without size *are* allowed:
```
&a {x:a}
```
This is *not* allowed:
```
&a a
```
## module system
Each file is a "compilation unit"
When compiling a compilation unit, the following inputs have to be provided:
- any amount of files containing signatures of exported definitions.
only definitions of the compilation unit that are in one of the signature files will get exported.
- any amount of other files containing imported definitions
Note that there is no practical difference between signature and source files.
### Example
Export signature file `List.li`:
```
type List[t] = 'End | 'Cons {head:t, tail:List[t]}
# not providing a function body makes it a function signature definition
def [t] `a++b`(a: List[t], b: List[t]) -> List[t]
def [t] Match.`a++b`(
value: List[t],
l: List[t],
r: MatchUtil.Var[List[t]]
) -> Option[{ r: List[t] }]
```
Import signature file `Option.li`:
```
type Option[t] = 'None | 'Some t
```
Compilation unit `List.lu`:
```
def [t] `a++b`(a: List[t], b: List[t]) -> List[t] {
# ...
}
def [t] Match.`a++b`(
value: List[t],
l: List[t],
r: MatchUtil.Var[List[t]]
) -> Option[{ r: List[t] }] {
# ...
}
```
### Notes
Each compilation unit gets compiled to implementation-specific bytecode.
Templated functions can only be compiled partially during a compilation unit. This will impact compile speeds.
Avoid templated functions wherever possible.
## Hide record fields in module signatures
Signatue:
```
type User = {name: List[Char], ...}
# the ... is used to indicate that this is a partial type definition
# User, as given here, can not be constructed, but name can be accessed
```
Compilation unit:
```
type User = {name: List[Char], password: List[Char]}
```
## Hide union variants in module signatures
Signature:
```
type DType = 'Int | 'UInt | 'Byte | ...
# users of this can never do exhaustive pattern matching on this
```
Compilation unit:
```
type DType = 'Int | 'UInt | 'Byte
# this compilation unit can actually do exhaustive pattern matching on this
```
## Note on hidden union variants / record fields
To make these work, the following is legal:
```
def example() -> 'Int | 'UInt | ...
def example() -> 'Int | 'UInt | 'Byte | 'Char {
# ...
}
```
## Extensible unions
```
extensible union Plan
extend Plan with 'ReadlnPlan Unit
extend Plan with 'WritelnPlan Unit
# pattern matching against these is always non-exhaustive.
# can only pattern match with the imported extensions
```
## Any type
in the stdlib:
```
extensible union Any
type Any.LambdaCalc = 'Apply {fn: Any.LambdaCalc, arg: Any.LambdaCalc}
| 'Scope {idx: Uint}
| 'Abstr {inner: Any.LambdaCalc}
def Any.toLambda(a: Any) -> Any.LambdaCalc
```
It gets automatically extended with every type ever used.
## Lenses
In the stdlib:
```
type Lens[t,f] = {get: t -> f, set: t -> f -> t}
def [a,b,c] `a=>b`(x: Lens[a,b], y: Lens[b,c]) -> Lens[a,c] {
{
get: t:a -> y:get(x:get(t)),
set: t:a -> f:c -> x:set(t, y:set(x:get(t), f))
}
}
```
Since `a:f1:f2` is the field access syntax, the lens creation syntax is similar: `&Type:field1:field2`
So you can do:
```
type Header = {text: String, x: Num}
type Meta = {header: Header, name: String}
&Header:header:text (myHeader, "new meta:header:text value")
# which is identical to:
(&Header:header => &Meta:text) (myHeader, "new meta:header:text value")
```
However, it is cleaner to use `with`:
```
myHeader with header:text: "new meta:header:text value"
```
## Pure IO
the `IO[t]` type contains IO "plans" which will be executed by the runtime, if they are returned by the main function.
```
def main() -> IO[Unit] {
print("hey") # warning: result (of type IO[Unit]) was ignored. expression can be removed
print("hello")
}
# this will only print "hello"
```
To make the above example print both "hey" and "hello", we need to chain the two IO types:
```
def main() -> IO[Unit] {
await _ = print("hey")
await print("hello")
}
# or just remove the _
def main() -> IO[Unit] {
await print("hey")
await print("hello")
}
# if you don't put in the second await:
def main() -> IO[Unit] {
await print("hey")
print("hello")
# error: expected IO[Unit], got IO[IO[Unit]]; did you forget an `await`?
}
```
await is kinda weird. here is the syntax:
```
# ( await x: a = ( expr1::IO[a] ) await (expr2::IO[b]) ) :: IO[b]
expr |= 'await', (identifier, '='), expr, 'await', expr
# ( await x: a = ( expr1::IO[a] ) ( expr2::b ) ) :: IO[b]
expr |= 'await', (identifier, '='), expr, expr
# ( await (expr1::a) ) :: a
expr |= 'await', expr
```
## Pure IO implementation
Something like this is done in the stdlib:
```
extensible union IO.Plan[r]
type IO[t] = 'Just {value: t}
| 'Map template r: {of: IO[r], map: r -> IO[t]}
| 'More template r: {plan: IO.Plan[r], then: r -> IO[t]}
def [a,b] `await a (a->b)`(io: IO[a], then: a -> b) -> IO[b] {
'Map {of: io, map: r -> 'Just {value: then(r)}}
}
def [a,b] `await a (a->await b)`(io: IO[a], then: a -> IO[b]) -> IO[b] {
'Map {of: io, map: r -> then(r)}
}
# in stdio:
extend IO.Plan[Uint8] with 'stdio.ReadByte {stream: Int32}
def stdio.getchar : IO[Uint8] = 'More {plan: 'stdio.ReadByte {stream: 0}, then: by -> 'Just by[0]}
def main() -> IO[Unit]
```
the runtime does something like this:
```
def [a] RUNTIME_EVAL(io: IO[a]) -> a {
match io {
'Just {value} -> value
'Map {of, map} -> RUNTIME_EVAL(map(RUNTIME_EVAL(of)))
'More {plan, then, finally} -> RUNTIME_EVAL(then(match plan {
'ReadStream {stream} -> 'ReadStreamIOResult {data: impure perform the io here lol}
_ -> impure error here "this runtime doesn't support this kind of IO" or sth
}))
}
}
def RUNTIME_ENTRY() {
RUNTIME_EVAL ( main() )
}
```