hafly (pronounced "half-lee") is a simple dynamic embeddable scripting language in Haskell.
You can try it out on the web repl, which is itself a Haskell webapp implemented with Reflex. (The implementation can be found here.)
Please note that hafly is still in pre-alpha, and the design, feature set, and syntax are by no means stable.
I wanted a simple, easy to learn scripting language that was easy to embed in pure Haskell projects (so it's easy to target, for instance, ghcjs as well as native targets) that still meshes well with Haskell idioms. (Simple expression language + a monadic block syntax that can be interpreted in an arbitrary monad? Hell yeah!) As far as I could tell, such a thing did not exist -- so I made one!
Comparison with other projects:
- hslua: Easy for beginners, embeddable, yet is not pure haskell (hard to use in GHCJS).
- codeworld haskell: Possibly another good option in this space -- an educational variant of Haskell that can be run in GHCJS.
- hint: Very nice -- yet pretty heavyweight, and Haskell itself is not nescesarialy the easiest for beginners.
- Various lisp/scheme implementations: Maybe not the easiest to embed. Arguably not the most intuitive syntax for beginners. Too dissimilar to Haskell.
I wanted something quick and no-nonsense (implementation-wise), and easy to extend with arbitrary Haskell types and functions without mucking about with a bunch of singletons and/or type families -- building a dynamic language at first seemed like the easiest way to do that.
Given sufficent time (and/or interested contributors!) I'll probably add optional static type checking capabilities in the future.
- Naturally sandboxed and extensible.
- Dynamic (single) dispatch.
- Multiple Dispatch (i.e. the dymically-typed version of multi-parameter type classes)
- Higher-order functions
- Everything is an expression
- Recursion
- What if Haskell... but with a Rust-like monadic syntax thrown in for good measure? (But I might add whitespace sensitivity later, IDK)
- Kotlin-esque string templating.
- IORef-backed ML-like references (only in IO!)
- Sequential blocks that can be bound to any monad.
- Flexible binding of names in sequential blocks (do notation++)โข.
- Symbols and polymorphic variants.
- Simple pattern matching.
- Flexible records -- because we're not cavemen.
- Record dot and Uniform Function Call Syntax.
- Functorial polymorphism (A generalizaton of Array Programming Languages)
- Monadic polymorphism (Compare effect polymorphism in koka)
fact = \n -> if(n == 0) 1
else n * fact (n - 1)
squared = \x -> x * x
> squared $ squared 2.5
39.0625
f = \x -> x + 2
g = \x -> x * 5
h = g of f
h' = f then g
> h 1
15
> h' 1
15
[1, 2, "hello", 4.2, [1, 2, "x", "y"]]
squared = \x -> x * x
> 4.squared
8
> 2.5.squared.squared
39.0625
main = {
x <- var 0;
printLn "The value of x is $x.";
printLn "Enter in a new value for x.";
newValue <- readLn then toInt;
x := newValue;
printLn "The value of x is now $x."
}
-- Interperted in a monad allowing for the scheduling of
-- actions in IO.
remindMe time timeToRemindAgain todo = schedule time {
result <- prompt "Don't forget! $todo";
case result {
Ok -> done;
Cancel -> {
schedule timeToRemindAgain {
remindMe timeToRemindAgain timeToRemindAgain todo
}
};
}
}
-- Interpreted in a "builder" monad for a UI.
-- builds up a record of the form:
-- [ name: "bob", age: 42 ]
entryForm = Column {
Row {
Text "Name: ";
nameEntry <- TextEntry
.bind name
};
Row {
Text "Age: ";
ageEntry <- TextEntry
.bind age
}
}
UI = Column {
Row {
Text "Click the button:";
button <- Button ":)"
};
when button.clicked {
popupDialog "You clicked the button!"
}
}
is equivalent to the following:
UI = Column {
button <- Row {
Text "Click the button:";
button <- Button ":)";
return button
};
-- Variables in nexted monadic blocks are automatically accessible in parent scopes
when button.clicked {
popupDialog "You clicked the button!"
}
}
Let's say you have a type of reactive state variables State a
which is a functor (in Hafly terms, we've defined a map
operator for it via multiple dispatch), and a state : a -> m (State a)
to introduce them in some monad m
. Hafly can automatically map
and bind
operations over State
s without extra ceremony:
UI = Column {
count <- state 0
btn <- Button "Click me!"
when btn.clicked {
count := count + 1
}
Text "You clicked me $count times!"
}
Compare with the "manual" version, where we have to apply map
rather than directly operating over the State
:
UI = Column {
count <- state 0
btn <- Button "Click me!"
when btn.clicked {
count := count + 1
}
btnText = count.map $ \num ->
"You clicked me $num times!"
Text btnText
}
To set up your own embedded interpreter, you need to build an InterpreterContext
-- which contains all the data (such as "built-in" functions and operators) needed to interpret a hafly program. Hafly comes with an optional base :: InterpreterContext
"standard library" of sorts that you can use to get started -- but this can be customized to your own needs.
Once you have that, with interpret :: InterpreterContext -> Ast -> Either TypeError Dynamic
you can take a hafly Ast
and interpret it as a Dynamic
value from Data.Dynamic
. You can then attempt to grab a value of some concrete Haskell type out of that Dynamic
with Data.Dynamic
's fromDynamic
.
Hafly comes with a few built-in functions for interpreting hafly programs in specific contexts. For instance, interpretIO
can be used to interpret a hafly program representing an IO action.