Imperative Functional Programming (1996) [pdf]
cs.bham.ac.uk
cs.bham.ac.uk
Scala has a lot going for it, not the least its interoperability with Java. But being a hybrid language is not zero-cost: its type inference for example is less powerful than Haskell's (it can infer fewer things; I've read this is because inheritance complicates things), and to me its type signatures are harder to read. Also, because pretty much every Scala function has "purity escape hatches" built-in, you can never be sure what does or doesn't have side-effects. And this is unrelated to purity, but Scala's null makes me sad.
What's being described here is well-known and easily available in typed FP languages. It's a tiny application of the general properties of monads, dressed up to not have to say the word "monad". The novel bit is to give it a link-bait title.
(>>=) :: Monad m => forall a b. m a -> (a -> m b) -> m b
Prelude> getLine >>= putStrLn
blah
blah
Prelude> do a <- [1, 2]; b <- [4, 5]; if (even a) then (return b) else (return a)
[1,1,4,5]
Prelude> do a <- Just 1; b <- Just 1; if (even a) then (return b) else (return a)
Just 1
Prelude> do a <- Just 4; b <- Just 2; if (even a) then (return b) else (return a)
Just 2
Prelude> do a <- Nothing; b <- Just 2; if (even a) then (return b) else (return a)
Nothing
-- depending on a computation that may not have succeeded
Then you could distinguish pure and impure code just from the types by assigning a type wherever effects need to be performed to get our effecty value. Prelude> :t getLine
getLine :: IO String
Prelude> :t putStrLn
putStrLn :: String -> IO ()
Prelude> :t (>>=)
(>>=) :: Monad m => m a -> (a -> m b) -> m b
Prelude> :t (>>=) getLine
(>>=) getLine :: (String -> IO b) -> IO b
Prelude> :t (>>=) getLine putStrLn
(>>=) getLine putStrLn :: IO ()
Then we could make a syntax to wrap it all up and let you write code as if it was actually sequential, imperative instructions. main = do
putStrLn "Enter your name please: "
name <- getLine
putStrLn ("Hello, " ++ name)
Maybe we could make a programming language around this, maybe name it after a mathematician that contributed to the development of lambda calculus.Monad was introduced to clean up the nesting and give it a nice syntax.
I have heard that transformer stacks can add "wrapping/unwrapping overhead", but haven't looked at it in depth, most of my Haskell work not being terribly performance sensitive at that scale.
Of for Christ's sake, I of course mean the bind and the `>>` and whatever that it desugars to. You aren't adding something to the discussion. But thanks for reminding me that syntactic sugar has no runtime overhead...
There seems to be a lot of potential for removing all cruft that comes from things like this, namely to remove the interpretive overhead. I used to think of "interpretive" as only those language executioners. But I suspect that a lot of constructs have them. Macros, or whatever mechanism you use to avoid that overhead should maybe have a place in languages that try to both be high-level and to be somewhat high performing.
You asked a question, I addressed the parts of it I could address. I don't have a full picture of what you do and don't understand, to reference in pruning my response.
If you're aware that do-notation is syntactic sugar then of course I would expect you to be aware that it has no runtime overhead - but I regularly encounter people confused on that first point. My comment adds to the discussion if you're one of those confused people (which apparently you aren't), if one of those people winds up reading the thread, and also to clarify your question for anyone else interested in answering.
My answer wasn't intended to be derisive in any way - I don't appreciate abuse in response.
mtl-style final encodings can help here, from what I understand. `mtl` has won any benchmark I've seen, over concrete transformer stacks, Free, and others.
What I do miss though (dearly) is that there is a certain cost as to what can be encapsulated. For instance, in Haskell, it is no longer so easy to hide side effects behind a pure interface.
This is not true. There are a number of ways to do this in Haskell.
Also this is tantamount to, "my programming language won't let me lie", and is often associated with an infacility with Haskell. I mean no offense, just speaking from experience.
You don't need to hide effects behind a pure interface once you're comfortable with the abstractions provided.
Edit: tel's comment is quite good as-is, but I'd like to add that part of what makes ST nice isn't just having an eliminator, but also so you have a more precise delineation of what "effects" could happen. IO is kind of a big type. Still very valuable.
Now I know that you can hide unsafePerformIO in the interface, and in this case I expect it is fine, but this is not easy, it feels like a hack.
Another example that I have personally watched play itself out in disbelief was in a fairly large Haskell project: There was need of a lookup table, with some 10000 elements. In such cases, I would have stored the lookup in a database, or a separate file at least, but because they wanted to maintain purity so badly, the solution was to write a 10000 line long Haskell file. But wouldn't it be nice if you could load a file and tell Haskell: don't worry, this won't change, so treat reads to this purely.
I never said such things were impossible, merely not easy, and I don't believe this is by accident. I am almost certain that such things are made difficult to prevent abusing such features, but that doesn't stop me from missing them :) .
import Control.Monad.ST
import Data.STRef
data Counter s = Counter { get :: ST s a, inc :: ST s () }
mkCounter :: STRef s Int -> Counter s
mkCounter v = Counter (readSTRef v) (incRef v) where
incRef ref = do
i <- readSTRef v
writeSTRef v (i + 1)
A little noisier since we note that reading the ref is an impure action and we track a phantom `s` parameter around in order to convince Haskell that certain uses of counters are pure. Now comp :: ST s Int
comp = do
v <- newSTRef 0
let c = mkCounter v
inc c
inc c
inc c
get c
This is technically a pure computation despite using an impure mechanism internally. We can get Haskell to prove this for us pureComp :: Int
pureComp = runST comp
Here `runST :: (forall s . ST s a) -> a` completes the proof that if the `s` variables in a computation are completely undetermined then that computation must be pure.I just need someone to convince me that I could have invented monad transformers too, because while I now think I can use most of the common monads by themselves, trying to do multiple things at once just ends in (metaphorical) tears.
An example of a monad in Haskell that is not put together via transformers, yet provides all their functionality, is RWS, being a combination of Reader, Writer and State in one hard-coded type/instance.
It is fairly straightforward to not let mutable variables muck up pure functions -- A pure function doesn't do anything but return a value. A pure function which computes the length of a list might be implemented by an imperative procedure that loops through the list and updates a counter, which is then returned. Whether a pure function is implemented in a functional or imperative way is... an implementation detail.
As far as IO effects are concerned, Haskell's approach let's IO values be first-class. You can use regular, pure functions on, where in a different system you might indeed have to try to keep the "pure" and "impure" code separate. And they say that Haskell programmers are afraid of IO... yeah, so afraid that it is treated as a first-class concept and value. :-)
http://www.se-radio.net/2008/08/episode-108-simon-peyton-jon...
For example, Scala is an imperative/functional programming language hybrid. Yes, you can use Scala as mostly imperative I suppose, but I've found you end up doing some things in a functional manner in many cases because it's far easier.
"The idea of "mostly functional programming" is unfeasible. It is impossible to make imperative programming languages safer by only partially removing implicit side effects. Leaving one kind of effect is often enough to simulate the very effect you just tried to remove. On the other hand, allowing effects to be "forgotten" in a pure language also causes mayhem in its own way.
Unfortunately, there is no golden middle, and we are faced with a classic dichotomy: the curse of the excluded middle, which presents the choice of either (a) trying to tame effects using purity annotations, yet fully embracing the fact that your code is still fundamentally effectful; or (b) fully embracing purity by making all effects explicit in the type system and being pragmatic by introducing nonfunctions such as unsafePerformIO. The examples shown here are meant to convince language designers and developers to jump through the mirror and start looking more seriously at fundamentalist functional programming."
To me that argument sounds like, "wrist pain is a source of misery for many developers, hence we should chop off our arms and program using gestures with our tongue, which turns out to be a much more elegant solution" (I'm exaggerating, of course). It's as if provably verifiable, safe, imperative programming languages don't exist, and haven't been used to write safety-critical system for quite some time (actually, verifiable imperative languages have been used much more than PFP languages to develop such systems).
Also, the appeal to lambda-calculus(/i) as a more "mathematical" approach to computation (as if that is a goal in itself) discounts the fact that LC -- even as a theoretical model -- has some serious drawbacks, such as the lack of a good model of computational complexity.
As for the rest, I guess Erik was being a bit provocative when he gave that talk. From my very limited experience of how he explain topics, he tends to be playful. Keep in mind when I watched a presentation he gave about Reactive Programming, he actually advocated the opposite of this talk! (i.e. that it's best not to be fundamentalist, and take what works from each paradigm).
My takeaway from all this is that some of the beauty and power of FP can only be discovered when you go "fundamentalist"; otherwise the temptation to use "escape hatches" and go the familiar route is too strong, and once you do that you miss alternative formulations/solutions of known problems. At least, this is my experience with Scala.