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.