[Haskell] Implicit parameters redux
Ben Rudiak-Gould
benrg at dark.darkweb.com
Tue Jan 27 15:04:21 EST 2004
This article attempts to describe in more detail the implicit-parameter-
based sequencing model that I was trying to develop in a recent thread. I
am getting more and more excited about this idea, and after reading this I
hope you'll understand why. Any comments are greatly appreciated.
(Especially if there's a fatal flaw -- you'd better tell me now and get it
over with.)
Review of implicit return values
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
In the syntax I proposed a few days ago, implicit values are attached to
ordinary values like this:
(123, %x = "foo", %y = 99)
(I now write %x instead of ^x for a reason described in the next section.)
The syntax resembles a tuple, and they are in fact tuples behind the
scenes (unlifted, and probably unboxed). But the semantics is not quite
the same: the example above is equivalent to the same example with %x and
%y reversed, and also to
((123, %y = 99), %x = "foo")
These implicit return values propagate upward through expressions in the
same way that implicit parameters propagate downward. They can be "caught"
at the root of an expression by matching against a similar syntax:
case expr of
(a, %x = b) -> ...
Implicit return values are merged by name as they propagate upward, using
mappend. It is a compile-time error if two values to be merged have
different types, or a single type that is not an instance of Monoid.
Another extension I proposed is that the "name" of an implicit return
value can include type parameters: thus %foo Int and %foo Char would be
treated as though they had different names.
Threading and branching implicit values
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Implicit values fall into two categories which I'll call "threading" and
"branching".
Linear implicit parameters (%name) are branching if they have a type which
is an instance of the class Splittable, and threading otherwise.
Ordinary implicit parameters (?name) are always branching: they behave as
though their types were trivial instances of Splittable, where split x =
(x,x).
Implicit return values are like linear implicit parameters: they're
branching or threading depending on whether their types are or are not
instances of Monoid. There is no trivial instance of Monoid for arbitrary
types, so implicit return values have no variant analogous to ?name. For
this reason I will write them %name (abandoning the ^name convention I
used before).
The intuition behind the terminology is this: if you've programmed in both
procedural and functional languages (and who here hasn't?) you know that
procedural code tends to have a sequential structure, and functional code
tends to be hierarchical. Threading parameters mirror procedural
code: because they cannot be split or merged, they are passed along in
sequential fashion from a single point of production to a single point of
consumption. Branching parameters mirror functional code: they either
percolate down the expression tree, splitting as they go, or they
percolate up, merging as they go.
Branching implicit parameters render the Reader monad obsolete
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
It has been noticed already that ordinary implicit parameters provide
essentially the same functionality as the Reader monad, but in a more
convenient and versatile way. The main advantages are:
* Different pieces of state are distinguished by descriptive names,
instead of by depth in the monad stack.
* Code need not be rewritten in monadic style.
These are big advantages. There's no point using Reader if you have
implicit parameters.
Branching implicit return values render the Writer monad obsolete
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
It's not hard to see that implicit return values provide a replacement for
Writer with the same advantages.
Threading implicit values render IO, ST, and State obsolete
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
I'll be arguing for the rest of this article that threading implicit
values can replace all state-threading monads, and provide advantages
similar to those above. In the case of IO and ST the advantage is even
greater. IO and (forall s . ST s) form a mutually exclusive infinite
family of monads: any piece of code can operate on at most one at a
time. My proposal has (or seems to have) no similar restriction. This
opens the door to fine-grained fragmentation of mutable state, something
people have wanted since the earliest days of monads in Haskell.
Input and output threads
~~~~~~~~~~~~~~~~~~~~~~~~
Along with its other arguments and return value, any function takes zero
or more threading implicit arguments and returns zero or more threading
implicit return values. I'll call the former "input threads" and the
latter "output threads".
These threads represent bits of mutable state. For example, corresponding
to the IO monad there is an implicit value %io :: RealWorld, and any
function which interacts with the world will have an input and output
thread of this name and type.
Example: polymorphic cells
~~~~~~~~~~~~~~~~~~~~~~~~~~
Using this system we can implement a form of polymorphic "mutable cell" in
Haskell. We want an interface something like this:
beginRef :: a -> (exists s . (Ref s, %refState s a :: State a))
readRef :: (%refState s a :: State a) =>
Ref s -> (a, %refState s a :: State a)
writeRef :: (%refState s a :: State a) =>
Ref s -> b -> ((), %refState s b :: State b)
endRef :: (%refState s a :: State a) => Ref s -> a
The purpose of the "exists s" in the first type is to express the idea
that different cells get different state types s, and hence that the
threaded states for different cells remain separate for merge-by-name
purposes. It's not clear what Haskell's current type inference algorithm
would make of this, however. (Question: is there a deep reason that
anonymous existentials like this are not allowed in Haskell?)
Other than that, the most interesting type is that of writeRef, which is
more polymorphic than might be expected. This is a kind of flexibility not
offered by monads: the input and output threads of a function need not
match. These cells can be safely "overwritten" with a value of a totally
new type.
This interface is easily implemented in Haskell (for a sufficiently
flexible definition of "Haskell"):
newtype State a = State a
newtype Ref s = Ref
beginRef val =
(Ref, %refState s a = State val)
readRef { %refState s a = State val } Ref =
(val, %refState s a = State val)
writeRef { %refState s a = State _ } Ref val =
((), %refState s b = State val)
endRef { %refState s a = State val } Ref =
val
Notice that the Ref doesn't actually hold any information: it's just there
to pull in the appropriate state. This is only useful if we don't have to
pass that state in explicitly, which is what the next section is about.
The above implementation (if it indeed typechecks) is a proof that this
style of ref is type-safe. Once we know that, we can implement it using
low-level mutability instead. In this implementation the State parameter
holds no information, and exists only to enforce sequencing. The value is
actually held in the Ref, and it's updated by a genuine mutation.
Refs of this kind are not sufficient, however: we must have an analogue of
STRefs also. The reason is that it can't always be determined at
typechecking time which cell is used by a particular piece of code. Any
cells which can't be distinguished by the typechecker must be placed in a
common state space, a la STRef. It's not clear that it needs to be
possible to place cells of different value types ("a" above) in the same
space, though, since they can always be distinguished by that type. If
it's not necessary, then this style of "parallel ref" can also be
implemented in Haskell (in terms of the above ref type, in fact), which
solves a long-standing theoretical difficulty with the ST monad.
Oleg's polymorphic heap also fits into this model. It doesn't seem to be
needed here, but it might be helpful in thinking about the Tieable class
described below.
Haskell's pure functional style and its monadic style can be thought of as
special cases of this threading model. The pure functional style is the
case of no input or output threads, and the monadic style is the case of
all threads passing through all expressions in sequence, with the same
names and types throughout.
Hiding the plumbing
~~~~~~~~~~~~~~~~~~~
I introduce an operator =<>=, called "tie", which causes the output
threads of its left argument to be passed as input threads to its right
argument. Matching of threads is done by name. Any threads left untied
become input or output threads of the full expression.
This operator presents a serious problem for the design of the system
since it cannot be implemented in Haskell, and cannot even be given a
type. Worse, generic functions which use it are also untypable. I think
this problem can't be avoided -- it's inseparable from the benefits of
this system. I'll come back to this later and describe a possible
solution. For now I'll assume that there's a solution and we have =<>=.
I'll assume =<>= returns the (explicit) values of its arguments as a pair
(though this may be needlessly inefficient). We should be able to get the
>>= behavior of passing the value of the first expression on to the next
by writing "let (a,b) = (expr1 =<>= expr2 a) in b".
I next introduce some sugar called "thread" which takes a list of
expressions and glues them together using =<>=. The "var <- expr" syntax
is also available to bind the explicit value of any expression except the
last. I can see no reason why this can't be recursive -- i.e. all bound
variables are in scope throughout the thread expression -- but I could be
missing something.
"thread" takes a sequence of expressions, but in contrast to "do" -- where
expressions really do form a linear sequence -- the expressions passed to
"thread" have dependencies forming an arbitrary dag, and any topo-sorted
order within the thread expression is equivalent. The sorting is necessary
only because of the linear nature of source code.
In fact, if there are no dependencies at all, we needn't thread the
expressions at all. If the cells r and s live in separate state spaces, we
can simply write
readRef r + readRef s
instead of something analogous to "liftM2 (+) (readRef r) (readRef s)",
since the type system knows that the refs are independent and needn't be
ordered relative to each other. This is the key to safe declarative
concurrency, which I'll return to later.
Breaking up the IO cartel
~~~~~~~~~~~~~~~~~~~~~~~~~
Since we're no longer limited to one thread at a time, there's no reason
that IO need consist of a single thread. We could have separate threads
for stdin, stdout, and stderr, for example, and code which only outputs
could safely run in parallel with code which only inputs.
However, it's difficult to see what divisions can be made safely, since
pretty much anything goes in the RealWorld. Sometimes stdin and stdout are
independent, but sometimes they're not. The solution is to leave this up
to the programmer.
We provide (among other things) a function (unsafe)detachHandle, which
takes a handle and the world, and returns two worlds, one containing only
that handle and one containing everything else. The proof obligation is
simply that the two worlds don't interact outside the Haskell
program. (The implementation guarantees that they don't interact inside.)
The first of these worlds should be parameterized by a state variable. A
new handle, also so parameterized, will have to be returned as well. I
don't think there's a way to statically prevent reuse of the old handle --
this must be checked at runtime. This check corresponds to the current
"half-closed" state of handles.
There should also be a sledgehammer function unsafeForkWorld, for cases
when the more precise functions aren't versatile enough. This is analogous
to unsafeInterleaveIO. I think that everyone's favorite sledgehammer,
unsafePerformIO, will also still be useful.
Declarative concurrency
~~~~~~~~~~~~~~~~~~~~~~~
This system is a natural fit with concurrency. To spawn concurrent
threads, a program divides up the IO world as it sees fit, then gives each
thread its own independent piece. Provided the proof obligation is met,
these threads cannot interfere with each other. MVar state must be shared,
of course, but needn't be tied to IO: MVars should exist in state-thread
families like STRefs.
The need for boxing
~~~~~~~~~~~~~~~~~~~
Aside from untypability, the present system has another serious flaw: a
function like
performTwice foo = thread { foo; foo }
will in fact perform foo's side effects only once! The reason is that
implicit values are not passed into functions along with arguments. Foo
will do its thing at the call point, and inside performTwice it will just
be an unthreaded value.
The core reason for this is that we don't explicitly specify when thread
parameters get applied to actual values. The monad system does not suffer
from this problem because you must explicitly place values in the monad
(with return) and explicitly extract them (with runX). We can solve the
analogous problem for threads by boxing the return value and its
associated threads into a single abstract entity, and making =<>= operate
on that.
Once we have a box, we can make that box an instance of a type class,
which suggests a solution to the problem of typing =<>=. We introduce a
class which for lack of a better idea I'll call "Tieable":
class Tieable a b c | a b -> c where
(=<>=) :: a x -> b y -> c (x,y)
Instances of Tieable must be automatically derived by the compiler. It's
not clear to me how the actual boxing and unboxing should take place, or
how the boxes should be named in the type language.
The ties that bind
~~~~~~~~~~~~~~~~~~
Can we derive an implementation of monadic bind from an implementation of
tie? In general no, but in certain cases yes.
The cases are those in which the types a and b in the definition of
Tieable are the same. That is, we have something like
instance (Tieable a a a) => Monad a where
m >> n = snd (m =<>= n)
m >>= f = let (a,b) = (m =<>= f a) in b
return = box
This is fortunate, since it means that IO and ST can continue to exist for
compatibility, and that flow-control transformers like ContT can be
applied to threaded values.
Arrows?
~~~~~~~
Question: how does the class Tieable compare to the class Arrow?
-- Ben
More information about the Haskell
mailing list