packages feed

Frank-0.2: test.fk

note
  This is a test source file in the programming language Frank.

note
  Layout in Frank is really dumb. Each left-anchored line begins a new
  top-level block, containing all the tokens until the next left-anchored
  line. Other spacing is unimportant. A block beginning "note" is a comment.

note
  Frank's type system relates three notions
    (1) value types, of things which *are*
    (2) computation types, of things which *do*
    (3) effect signatures, which specify what one *can*

note
  Some perfectly ordinary datatypes follow. List is parametrized.

data Nat
  = zero
  | suc Nat

data List X
  = nil
  | X :: (List X)

note
  Type constructors are always prefix and capitalized.
  Type variables are always capitalized.
  Value constructors are uncapitalized and form *templates*
    with places given by the things which are types.
  So :: is infix.

note
  Here are some perfectly ordinary functions.

Nat + Nat [] Nat
zero  + y = y
suc x + y = suc (x + y)

(List X) ++ (List X) [] List X
nil        ++ ys = ys
(x :: xs)  ++ ys = x :: (xs ++ ys)

note
  These definitions declare a function template, where the types show
  the places for inputs. The [] (pronounced "returns") marks the end
  of the template and the beginning of the output type.
  You can drop [] (), if the output is the unit type.

dull Nat
dull zero = ()
dull (suc n) = dull n

note
  An effect signature is also prefix and capitialized. It describes a
  bunch of commands. Again [] means "returns" and you can drop [] ().

sig State S
  = get [] S
  | set S

note
  Here's how to describe a way to run a stateful process.
  The type [State S ? X] is the type of *requests* from stateful processes.
  A request is either [x] ("return x", where x is an X) or
  [command ? continuation].
  Frank's ? construct allows a function from a request type to
  *handle* a process.

state S [State S ? X]  [] X
state s [x]            = x
state s [get ? k]      = state s ? k s
state _ [set s ? k]    = state s ? k ()

note
  Unlike eff, Frank does not automatically compose the handler to the
  continuation. Ultimately, there's no great difference in expressivity,
  but this way is a little more first-order, and it's easier for the
  handler to evolve. Here, for example, we handle the continuation for
  set s using a suitably updated handler.


note
  Here's the Abort effect signature and one way to run it.
  Frank currently does not allow polymorphic commands, so let's
  use the empty type, {}.

sig Abort
  = aborting [] {}

data Maybe X
  = yes X
  | no

catch [Abort ? X] [] Maybe X
catch [x]             = yes x
catch [aborting ? k]  = no

note
  To invoke a command with no arguments, use a postfix "!". Without
  arguments, a function symbol stands for the function itself, not the
  result of invoking it. f is pure, but (f !) may not be.

  Our aborting command has no arguments, so needs a !. It returns an
  element of {}, which can be mapped to any type by the postfix {} operator,
  pronounced "bunk".

abort [Abort] X
abort = aborting! {}


note Nonempty {..}, pronounced "thunk", make a value type from a
  computation type. You can think of a thunk as a "suspended
  computation", and the fact that "suspenders" is American for
  "braces" is handily mnemonic.  Frank distinguishes value type X (of
  X values) from value type {[] X} of suspended computations that
  return X values.

  That distinction allows us to write control operators.

note
  Bool is built in, as if
  data Bool = tt | ff

if Bool then {[] X} else {[] X} [] X
if tt then t else e = t!
if ff then t else e = e!

note The above if-then-else chooses which thunk to invoke. To
  construct thunks, write expressions in {..}, so it looks
  suspiciously like C. We may observe that

    catch ? if tt then {zero} else {abort!}  =  yes zero

  Contrast with the conditional function.

cond Bool X X [] X
cond tt t f = t
cond ff t f = f

note We'll find that

    catch ? cons tt zero (abort!)  =  no

  because the (abort!) is evaluated.


note The reason [] is a bracket, not a :, is that it isn't always
  empty.  It contains a bunch of signatures for effects the function
  is allowed to do. Here's safe subtraction, which we can write
  directly, thus.

Nat - Nat [Abort] Nat
x     - zero   = x
zero  - suc y  = abort!
suc x - suc y  = x - y

note
  You can invoke subtraction only where Abort is enabled, e.g., inside
  catch. Frank programs are checked with respect to an ambient bunch of
  signatures. The ? construct locally extends the ambient bunch of
  signatures.

note
  Here's a little higher-order entertainment for you. A function type
  is a computation type, and can thus be thunked. Thunks are always
  pure, even if the function being thunked might perform some effects.
  The inner [] could indeed contain some signatures, and if it does,
  well, those signatures need to be enabled anywhere you *invoke* the
  function.

map {A -> [] B} (List A) [] List B
map f nil        = nil
map f (a :: as)  = f a :: map f as

note
  You are at liberty to suppress an empty [] in a computation type,
  but be aware! When you write a function type, each [sigs] it contains
  is really an *action* on the ambient signature, meaning "sigs extending
  the ambient signature". That's to say, the types are ever so slightly
  effect-polymorphic. For map, below, the meaning is that whatever effects
  are available when map is invoked may be used at each element, too. Our
  map is really Haskell's "mapM". The upshot is that you can write this:

subs (List Nat) Nat [Abort] List Nat
subs xs n = map {m -> m - n} xs

note
  But there's a subtlety. Consider trees represented with functional
  branching. Each node packs a *pure* function from Bool.

data Tree X
  = leaf X
  | node {Bool -> Tree X}

note
  The following definition of tree mapping is disallowed

  tmap {A -> B} (Tree A) [] Tree B
  tmap f (leaf a)  = leaf (f a)
  tmap f (node g)  = node {b -> tmap f (g b)}

  The type of f, longhand, is {A -> [] B} meaning that f can do
  whatever the ambient effects are when tmap is invoked. But we
  use f inside a node, where the function must be pure, so the
  typechecker refuses.

  The following is, however, accepted. Here, the signature in f's type
  says {}, which as a signature is pronounced "pure". Its action on
  the ambient signature is to empty it.

tmap {A -> [{}] B} (Tree A) [] Tree B
tmap f (leaf a)  = leaf (f a)
tmap f (node g)  = node {b -> tmap f (g b)}

note
  You can't make a dangerous, nearly broken tree, like this.

  subt (Tree Nat) Nat [Abort] Tree Nat
  subt xt n = tmap {m -> m - n} xt

note
  But safe mapping is ok.

addt (Tree Nat) Nat [] Tree Nat
addt xt n = tmap {m -> m + n} xt

note
  Of course you can do something like.

mkNode (Tree X) (Tree X) [] Tree X
mkNode l r = node {tt -> l | ff -> r}

tmapOk {A -> B} (Tree A) [] Tree B
tmapOk f (leaf a) = leaf (f a)
tmapOk f (node g) = mkNode (tmapOk f (g tt)) (tmapOk f (g ff))


note
  Here's a bit of stateful fun.
  The defined command "bong" returns the value of a Boolean state
  but flips it.

not Bool [] Bool
not tt = ff
not ff = tt

X but () [] X
x but c = x

bong [State Bool] Bool
bong = get! but set (not (get!))


note
  So if we define pairing...

data Pair A B = A & B

note
  ...we get

   state ff ? bong! & bong!  =  ff & tt


note
  Bits and Pieces for examples

two [] Nat
two = suc (suc zero)

note
  I must allow the definition of non-functional values.

note
  main [] Nat
  main = two! + two!

note
  main [] Maybe Nat
  main = catch ? if tt then {zero} else {abort!}

note
  main [Abort] List Nat
  main = subs (two ! :: (two ! :: nil)) (suc zero)

main [Console] Char
main = inch!