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
Ben, Could you explain in an extremely dumbed-down way what this is? It would be great if there were examples of 1) Some common, simple, and useful code in Haskell. 2) Same code using Implicit Parameters with a discussion of how it is better. Thanks, David J. Sankel
Here's an example of implicit return values from a project I worked on recently, followed by an example of the thread idea. Suppose I've written a decompiler -- it takes binary object code and produces an abstract syntax tree representing source code. A very simplified version of the output type might be type StatementBlock = [Expr] data Expr = Arith Expr String Expr -- e.g. Arith (Literal 5) "+" (Literal 8) | Assign Expr Expr | ProcCall Expr [Expr] | Literal Int | TheProcedure Int | ... The Int field of "TheProcedure" is the raw address of the beginning of the procedure in the file. So code like "foo(1,2,3)" will be represented as something like "ProcCall (TheProcedure 51034) [Literal 1, ...]" I want to produce source code as output, so I write a function with type StatementBlock -> String: showStatement exprs = concat [ showExpr x ++ ";\n" | x <- exprs ] showExpr (Arith left op right) = showExpr left ++ op ++ showExpr right showExpr (ProcCall proc args) = showExpr proc ++ "(" ++ join "," (map showExpr args) ++ ")" showExpr (Literal n) = show n showExpr (TheProcedure addr) = "procedure" ++ show addr The last line leaves something to be desired -- it chooses very unfriendly names for the procedures. As a matter of fact I have various heuristics for choosing more helpful names for procedures, and I also allow the user to supply a configuration file with names. So I encapsulate all this in a table of names and pass it to showExpr, and I get code like showExpr names (TheProcedure addr) = lookupProcedureName names addr But the rest of showExpr and showStatement get needlessly ugly, because they have to pass a "names" parameter to every recursive call. This is where ordinary implicit parameters become useful. I replace "names" with "?names" and it gets passed around for me. Now the decompiler may produce code which refers to procedures I don't "know about" (haven't decompiled). I can indicate this in the source code I produce: showExpr names (TheProcedure addr) = case lookupProcedureName names addr of Just name -> name Nothing -> "(*** unknown procedure ***)" But I'd like to also collect these for later use -- say, to list as part of a summary printed at the end. There are various ways I could do this, but let me concentrate on this one: showExpr (TheProcedure addr) = case lookupProcedureName ?names addr of Just name -> (name, []) Nothing -> ("(*** unknown procedure ***)", [addr]) showExpr (Literal n) = (show n, []) showExpr (Arith left op right) = (x++op++y, p++q) where (x,p) = showExpr left (y,q) = showExpr right This strategy lets us collect a list of unrecognized addresses at the top, as a second return value. But the code gets very ugly -- much worse than the implicit parameter case, in fact, since Haskell doesn't have a convenient notation for multiple return values. I could hide this with a modified ++ operator: (x,p) <++> (y,q) = (x++y, p++q) Then I could write: showExpr (Arith left op right) = showExpr left <++> (op, []) <++> showExpr right Better, but not great. Implicit return values provide a much cleaner solution: just write showExpr (TheProcedure addr) = case lookupProcedureName ?names addr of Just name -> name Nothing -> ("(*** unknown procedure ***)", %unknown = [addr]) and you're done. None of the other cases need to be modified (unless they also produce unknown addresses). This need to produce some form of statistical information "on the side" comes up fairly frequently in my code. Now state threading. Consider the following silly imperative program in C: char name[100]; int i; puts("What is your name?"); gets(name); for (i = 0; name[i]; ++i) name[i] = toupper(name[i]); puts("Your name in uppercase is:"); puts(name); There's all kinds of mutation and I/O going on here. In imperative programming there's a "current state", which includes things like the screen and the keyboard buffer and the array "name", and you give a list of commands which do something to that state, in a particular order. A pure functional language doesn't have any implicit state. You can model state by passing around a state variable, e.g. main :: World -> World main theWorld = let theWorld' = puts theWorld "What is your name?" (name,theWorld'') = gets theWorld' ... in theWorld''''''' This isn't very convenient. Worse, theWorld can't really represent the world, because you can reuse old values, and that isn't possible in reality. We can solve both problems by abstracting away from the world-passing. We think of puts and gets and similar functions as world-transformers, and we allow the programmer to attach the output of one to the input of another. This is the IO monad model. There's no way to duplicate the world because there's no transformer with one input and two outputs. (Well, there is, actually: unsafeInterleaveIO.) The program looks like this at a high level, with the world-threading shown: +------+ +------+ +----------+ +------+ +------+ | | | | | | | | | | World >-| puts |->-| gets |->-| for-loop |->-| puts |->-| puts |-> | | | | | | | | | | +------+ +------+ +----------+ +------+ +------+ Though this closely reflects the structure of the C program, it's really more ordering than we need. The first and fourth stages don't use the array, and the third stage doesn't do any I/O. We really have two completely independent state threads, with only some stages using each one: ,------>-------. +------+ +------+ | +----------+ | +------+ +------+ I/O >-| |->-| |-' | | `-| |->-| |-> | puts | | gets | | for-loop | | puts | | puts | | | ,-| |->-| |-. | | ,-| |-> +------+ | +------+ +----------+ | +------+ | +------+ Array >------' `---->-----' Now we can see that there's no particular ordering required between the for-loop and the "following" puts step: they could happen in the other order, or even in parallel (in different OS threads, or on different processors in an SMP machine). This program is interactive, so it's important that input and output be synchronized. But if it were a batch filter, we could separate I/O into I and O as well, and the program would become even less linear. Another example: in a web server, each connection could be represented by a different state thread, and the server could spawn separate OS threads for each connection without duplicating the whole world. The thread model I'm proposing sets up this fancier plumbing automatically, from a notation almost the same as "do". Haskell already has a notion of independent state threads, but it's not general enough for the above example. You can't put the array in an ST thread, because there's no way to write "gets" such that it can both read from an IO handle and write to an ST array. So my proposal has benefits other than better concurrency: it makes it easier to use mutable state without monolithic monads like IO gobbling everything up. -- Ben
In article <Pine.LNX.4.21.0401271448560.7939-100000@dark.darkweb.com>, Ben Rudiak-Gould <benrg@dark.darkweb.com> wrote:
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.
This bit doesn't seem very polymorphic-friendly? -- Ashley Yakeley, Seattle WA
On Thu, 29 Jan 2004, Ashley Yakeley wrote:
Ben Rudiak-Gould <benrg@dark.darkweb.com> wrote:
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.
This bit doesn't seem very polymorphic-friendly?
Well, there can be type variables there too. The issue is that there needs to be a source of fresh names for newly-created state threads, and the simplest solution I could think of was to return an existentially-quantified %foo s. It's supposed to work along the lines of a (Num a, Num b) context, where the type checker doesn't merge the constraints because it can't prove they're equal, even though it also can't prove they aren't. It's not clear that it's formally sound, though. Also, it would be nice if the type-class system could be implemented in terms of implicit parameters (plus sugar), and this extension would help with that. It might be possible to just parameterize the type of the implicit parameter instead of its name, and decree that merging happens by name and type. -- Ben
participants (3)
-
Ashley Yakeley -
Ben Rudiak-Gould -
David Sankel