## 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 Something like this is done in the stdlib: ``` extensible union IO.Plan[r] type IO[t] = 'Just {value: t} | 'More template r, f: {plan: IO.Plan[r], then: r -> IO[f], finally: f -> t} def [a,b] `await a (a->b)`(io: IO[a], then: a -> b) -> IO[b] { match io { 'Just {value} -> 'Just {value: then(value)}, 'More &a {finally,...} -> a with finally: r -> then(finally(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], finally: x->x} def main() -> IO[Unit] ``` the runtime does something like this: ``` def [a] RUNTIME_EVAL(io: IO[a]) -> a { match io { 'Just {value} -> value 'More {plan, then, finally} -> 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() ) } ```