Tree @master (Download .tar.gz)
picoml.md @master — view markup · raw · history · blame
PicoML
PicoML is an ML- and Haskell-inspired language that is designed to be easy to implement, straightforward, consistent, and small.
fact : Int -> Int
0 = 1
n = n * fact (n - 1)
main () = putln ("fact 5 = " <> fact 5)
Type declarations are done either with the data
or the record
keywords:
data List a =
[ Nil
, Cons a (List a)
]
record Stream a =
{ .head a
, .tail (Stream a)
}
Types created with data
are always eager and types created with record
are always lazy, so you can easily create infinite streams:
streamZip : (a -> b -> c) -> Stream a -> Stream b -> Stream c
streamZip f s1 s2 =
{ .head = f s1.head s2.head
, .tail = streamZip f s1.tail s2.tail
}
fibs : Stream Int =
{ .head = 1
, .tail = streamZip _+_ fibs fibs.tail
}
Recursive bindings are allowed in a type-directed way, so a binding whose type is a function or a record will allow recursive bindings, but a binding whose type is data will not allow a recursive binding.
listZip : (a -> b -> c) -> List a -> List b -> List c
f (Cons x xs) (Cons y ys) = Cons (f x y) (listZip f xs ys)
_ _ _ = Nil
tail : List a -> List a
tail (Cons x xs) = xs
tail Nil = error "tail of empty list"
-- this definition will fail, because one cannot recursively refer to
-- value bindings
badFibs : List Int
= Cons 1 (zip _+_ badFibs (tail badFibs))
The language is not pure, but all bindings are immutable, so reference cells are provided in the stdlib:
impFact : Int -> Int
n = let acc = ref 0
; let num = ref n
; while { !num > 0 }
{ acc := !acc * !num
; num := !num - 1
}
; !acc
This also demonstrates the other usage of curly braces: they are sugar for
an anonymous function that takes a ()
argument, i.e. the above impFact
definition is equivalent to
impFact : Int -> Int
n = let acc = ref 0
; let num = ref n
; while (() -> !num > 0) (() ->
( () ->
acc := !acc * !num
; num := !num - 1
)
; !acc
This means that while
is a function included in the stdlib, as well:
while : Thunk Bool -> Thunk () -> ()
cond body = if | cond () -> (body () ; while cond body)
| else -> ()
-- because elsewhere
type Thunk a = () -> a
Finally, PicoML has a mechanism for supplying values implicitly. The
implicit
keyword can be used on value bindings and on function
arguments. The language will then infer, based on the type, whether or
not the user supplied the argument, or whether it needs to reach for
the implicit argument:
implicit n : Int = 5
shout : implicit Int -> ()
x = println (show x)
main =
{ shout 8 -- prints 8
; shout -- prints 5
}
If there are multiple implicit values in scope whose types match, then PicoML will raise a compile-time error:
implicit n : Int = 5
implicit m : Int = 6
main = { shout } -- will not compile, because of the ambiguity
-- as to which implicit to choose
This, combined with manual dictionary-passing, is how typeclass-like functionality is implemented in PicoML
record Monoid m =
{ .mempty m
, .mappend (m -> m -> m)
}
implicit additiveMonoid : Monoid Int =
{ .mempty = 0
, .mappend = _+_
}
multiplicativeMonoid : Monoid Int =
{ .mempty = 1
, .mappend = _*_
}
mconcat : implicit (Monoid a) -> List a -> a
m (Cons x xs) = m.mappend x (mconcat m xs)
m Nil = m.mempty
main =
{ print (mconcat [2,3,4]) -- prints 9
; print (mconcat multiplicativeMonoid [2,3,4]) -- prints 24
}