24 days of Hackage, 2015: day 13: hint: runtime eval for Haskell
Dec 13, 2015 · 6 minute read · CommentsHaskellHackagehintevaldynamicGHCLispJavaScriptJava
Table of contents for the whole series
A table of contents is at the top of the article for day 1.
Day 13
One hallmark of a “dynamic language” such as Lisp and JavaScript is the ability to evaluate code at runtime inside a running process. Since runtime loading of classes is a fundamental feature of Java, is Java a “dynamic language” then? I think the terms “static language” and “dynamic language” are not very useful terms, and a comparison of language and compiler and development environments should focus on specific features of the user experience and where the boundaries lie in the semantics. One tricky thing is that a lot of what is interesting is actually implementation-dependent.
For example, the Haskell standard says nothing about runtime eval
,
so there is some sense in which Haskell considered as a strictly
defined “language” has no support for it. But if we consider the GHC
implementation and its ecosystem, which is dominant today despite the
existence of other Haskell implementations, there’s a lot of tooling
that is “dynamic”, in the sense of being able to access GHC APIs in
one of many different ways.
Edward Yang recently wrote an interesting blog post “The convergence of compilers, build systems and package managers” on a subset of the general issue of what can access what for the sake of tooling. It didn’t touch on runtime evaluation, which is an entire topic in itself.
For today, I decided to mention that you can already do runtime eval
of GHC Haskell code using the package
hint
and offer the
thought that maybe we might want something less ad hoc than
third-party packages like this.
Why dynamic loading and evaluation of Haskell code?
For me, it always comes down to being jealous of the Lisp world.
There are times when I have wanted to be able to do dynamic loading and evaluation of Haskell code, and wished I were in Lisp. The main example is when supporting user-written “plugins” that can be loaded from a source file or even typed at a custom REPL. The cleanest way of doing such a thing is to create and implement a limited domain-specific language and write a parser, type checker (if the DSL is typed), compiler/interpreter for it. But why do all that if we can just allow using the full power of Haskell instead?
Luckily, I found libraries such as hint
that enabled me to do what I
wanted. I’ll show a toy example of the kind of thing that I have done.
The task
Imagine a program that does sorting, and allows the user at runtime to
submit a custom sorting function to have it be used in place of the
default options. For example, the user could have specified the path
of a Haskell source file OurSorter.hs
as a command-line argument, or
the program could have a preferences dialog box allowing the user to
enter the text of a sorting function.
To make things even more interesting, let’s say that the sorting function to be specified has to be polymorphic, constrained only to require comparison:
userDefinedSort :: Ord a => [a] -> [a]
How do we load this type of function at runtime?
A necessary type wrapper
The first thing to get out of the way is that we cannot load a
function of type Ord a => [a] -> [a]
directly, because of lack of
current GHC support for
impredicate types. However,
there is a trick we can play, which is to wrap such a type in a
newtype
, along with using the higher-rank type language feature. I
learned this trick from this article on impredicative types.
{-# LANGUAGE RankNTypes #-}
module SortWrapper (Sort(..)) where
newtype Sort =
Sort { getSort :: forall a. Ord a => [a] -> [a] }
Now we can try to load values of type Sort
instead of type Ord a =>
[a] -> [a]
.
How to load?
Let’s create an API called loadSort
that enables loading a
particular Sort
by looking for it by module and name. Here’s an
HSpec test that illustrates that we want to be able to load a Sort
and use it on different types of lists. We’re using
Language.Haskell.Interpreter
to do the work:
module HintExampleSpec where
import SortWrapper (Sort(Sort))
import HintExample (loadSort)
import qualified Language.Haskell.Interpreter as I
import Test.Hspec (Spec, hspec, describe, it, shouldBe)
import Test.Hspec.QuickCheck (prop)
spec :: Spec
spec =
describe "hint" $ do
it "dynamically loads a correct polymorphic sort function" $ do
Right (Sort ourSort) <-
I.runInterpreter (loadSort "OurSorter" "ourSort")
ourSort "ebcad" `shouldBe` "abcde"
ourSort [1 :: Int, 5, 4, 3, 7] `shouldBe` [1, 3, 4, 5, 7]
it "dynamically loads a wrong (only head) sort function" $ do
Right (Sort onlyHead) <-
I.runInterpreter (loadSort "OurSorter" "onlyHead")
onlyHead "ebcad" `shouldBe` "e"
onlyHead [True, False] `shouldBe` [True]
A sample “plugin”
We created a sample “plugin” in a directory whose source code is not compiled into our main program. Imagine that the user has a separate plugins directory.
The loader
module HintExample where
import SortWrapper (Sort)
import qualified Language.Haskell.Interpreter as I
import Language.Haskell.Interpreter (OptionVal((:=)))
-- | Dynamically load a 'Sort' implementation from a file.
-- src is needed to pick up our SortWrapper.
-- sort-plugins is a sample user plugins directory
loadSort :: I.MonadInterpreter m =>
String -- ^ module name
-> String -- ^ function name
-> m Sort
loadSort moduleName functionName = do
I.set [I.searchPath := ["src", "sort-plugins"]]
I.loadModules [moduleName]
I.setImports [moduleName, "SortWrapper"]
I.interpret (moduleName ++ "." ++ functionName) (I.as :: Sort)
The most interesting part is the use of the interpret
function that
has type
interpret :: (MonadInterpreter m, Typeable a) => String -> a -> m a
taking a string and a witness for a monomorphic type in order to tell
interpret
what runtime dictionary for Typeable
to use (the modern
standard way for the library to have done this would have been to use
a
Proxy
).
So there we have it: runtime eval in GHC Haskell. What hint
provides
is fairly primitive, but I found it useful.
Conclusion
I’d like to see more official support for dynamism in environments for
languages such as Haskell. This does require access to compiler
internals or official APIs, but I think this is the way to
go. Principled phase separations are important but so is
integration. I like that hint
exists to allow me to dynamically load
GHC Haskell code.
All the code
All my code for my article series are at this GitHub repo.