Sintrastes / hafly

Dynamic embeddable scripting language in Haskell.
https://sintrastes.github.io/hafly
GNU Affero General Public License v3.0
3 stars 0 forks source link
dynamically-typed embeddable-scripting-language functional-programming haskell scripting

hafly

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.

Why?

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:

Why dynamically typed?

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.

Features / Progress

Examples

Recursive Function Definitions

fact = \n -> if(n == 0) 1 
    else n * fact (n - 1)

Function Application Operator

squared = \x -> x * x

> squared $ squared 2.5
39.0625

Function Composition

f = \x -> x + 2
g = \x -> x * 5

h  = g of f
h' = f then g

> h 1
15

> h' 1
15

Heterogenous Collections

[1, 2, "hello", 4.2, [1, 2, "x", "y"]]

Uniform Function Call Syntax

squared = \x -> x * x

> 4.squared
8

> 2.5.squared.squared
39.0625

ML-like References (Mutable Variables)

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."
}

Scheduling Task

-- 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
            }
        };
    }
}

User Interface

-- 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
    }
}

Do Notation++ (WIP)

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!"
    }
}

Reactive Polymorphism (WIP)

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 States 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
}

How does it work?

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.