slice by feature

In the Epigram-hacking business, we run slap bang into the Expression Problem all the time. Let me be clear that this is not a solution. It's just a cheap presentational trick.

We're implementing a programming language. It has a bunch of features, each of which contributes to the syntax, the typing rules, the evaluation rules, and all sorts of other stuff. We work in a functional setting where datatypes are closed: adding new functions is easy; adding new data is hard. Adding a new feature to the language means scattering small changes all over the codebase. I always forget a few, which causes embarrassing match failures, but what's worse is the code comprehension and documentation problem. I'd much rather write (or read) a piece about, say, how quotient types are implemented, than to have to pick through a zillion different files to what's going on and whether it holds together.

So here's a mechanism to keep code in files you might want to read, but compile it in the right place anyway. It's a bit of a rickety hack, and the syntax is highly dubious: suggestions for improvement welcome!

code accumulation

A code accumulation is a bunch of lines of code, named by a Capitalized identifier. You send code to an accumulation like this:

import -> MyAccumulation where
  myLine1
  myLine2
  myLine3

You dump the whole of an accumulation into your code with a line like this:

import <- MyAccumulation

She replaces the latter with all the hunks of code accumulated from the former, in an unspecified order, and at the indentation level of the import. You don't have to be pernickety about counting spaces, but scope and type checking don't happen until after the reassembly happens.

In any given module, code blocks are accumulated from the module itself, and inherited from the .hers files of imported modules: they vanish from the source itself, but are exported in the .hers file for the module at hand. Once these blocks have been whipped out, any accumulations imported get pasted in. This happens before she does all the other stuff, so you can have blocks of pattern synonyms or whatever.

Export before import means you can use this stuff even within one module. But it also means you can't build new accumulations by combining old ones.

example

I define an expression language with variables, and maybe some other stuff.

data Exp :: * -> * where
  V :: x -> Exp x
  import <- Exp
  deriving Show

I'm not using 6.12 yet, so I've got to write my own Traversable instance.

instance Functor Traversable where
  traverse f (V x) = V <$> f x
  import <- TravExp

I write an evaluator for expressions

eval :: Exp x -> (x -> Val) -> Val
eval (V x) = ($ x)
import <- EvalExp

By Hutton's Razor, Val is Int, and we need

import -> Exp where
  N :: Int -> Exp x
  (:+:) :: Exp x -> Exp x -> Exp x
import -> TravExp where
  traverse f (N i) = pure (I i)
  traverse f (s :+: t) = pure (:+:) <$> traverse f s <*> traverse f t
import -> EvalExp where
  eval (N i) = pure i
  eval (s :+: t) = pure (+) <$> eval s <*> eval t

As long as the module import trail passes the chunks along, it'll all work swimmingly.

For a longer example, see Pig.lhs importing from Hig.lhs and Jig.lhs, trying out various language features for compatibility.

gremlins

  • Judicious use of the line pragmas would be a considerable boon here, relating errors back to their actual source locations.
  • No recursive imports.
  • You can't accumulate imports.
  • You can easily engineer to duplicate stuff.