gdritter repos documents / master scraps / picoml.md
master

Tree @master (Download .tar.gz)

picoml.md @masterview 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
  }