Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

Because how else would you implement Agda? Hah!

The idea of managing side effects as a type (monads) still hasn't seemed compelling to me in terms of development time. Here I agree with Liskov in saying that it's a bit over the top[0]. Most of the quality I see with Haskell has to do with strict types, not the handling of I/O errors as types themselves.

Not that I want to bash it.. Learning haskell has actively changed how I approach all my C/C++ development, and gotten me far far far into the weeds of now learning Agda as a prototyping language for designs/semantics[1].

The world may or may not need Haskell. However it's certainly a better place now that it has it.

[0]: She said this at a talk she gave at work.. It was similar or the same as her "The Power of Abstraction" talk, dunno if she makes the same comment in every presentation though.

[1]: http://www.youtube.com/watch?v=vy5C-mlUQ1w



The big win of "managing side effects as a type" is not that effectful code gets an IO type but that everything else does not. Thus the lack of IO in a function's type assures you, reliably, that the function does not cause side effects or depend upon the state of the outside universe. This lets you corral effectful code and keep the bulk of your code "pure" and easy to reuse and reason about.


I would love to use Haskell for the algorithmic part of my code and some other language for the rest of it. I think that is what he is trying to get across. Just get rid of the IO monad and allow only call calls to Haskell from this other language — similar separation but without all the weirdness (IMHO).


Umm, doesn't Haskell sort of provide this with do notation? This is how programming simple things in Haskell feels like to me -- I build a lot of simple pure functions, and then bring them together in do blocks, which feel like a completely different, imperative language.


The fact that you have to use the IO monad, to me, feels like something completely ugly and different from what I get from haskell algorithm wise. IMHO.


I happen to find the IO monad incredibly beautiful and elegant. Haskell lets me define my own control structures for combining IO actions in a far more powerful and elegant manner than other languages.


While that is true, you could have accomplished the same thing through a special syntax that marked a function as "pure", and establishing the constraint that impure functions cannot be called by pure ones. And then only allowed I/O through a set of impure base functions.


As Erik Meijer said, there are many ways to be dirty, but only one way to be pure.

Haskell allows you to annotate classes of side effects such as "changes state" or "might throw exception", not necessarily the full IO monad, so annotating as pure doesn't make sense.


While (map) is pure, (map print) is not. Thus map is a higher-order function that can create both pure and impure functions depending upon the purity of its arguments. How then would your syntactic scheme allow for higher-order functions?


Actually, in Haskell (map print) is pure; it just doesn't do what you expect it to:

    >>> map print ["hello","world","!"]
    <interactive>:2:1:
    No instance for (Show (IO ())) arising from a use of `print'
    Possible fix: add an instance declaration for (Show (IO ()))
    In a stmt of an interactive GHCi command: print it
What's going on here? Let's look at the type of (map print) to find out:

    >>> :t (map print)
    (map print) :: Show a => [a] -> [IO ()]
(map print) is a function which takes a list of values of type a -- such that a can be shown as a String; hence the Show a => constraint -- and returns a list of values of type IO () (pronounced IO unit). These are monadic values representing computations that perform the actual IO. Hence, (map print) is a pure function which carries no side effects.

So, what the heck do we do with this strange list of IO ()s? Well, one answer is to pass them to sequence_:

    >>> sequence_ (map print ["hello","world","!"])
    "hello"
    "world"
    "!"
Ahhh, so now we get to the impure function: sequence_! Actually, sequence_ is a pure function as well. Its type is:

    >>> :t sequence_
    sequence_ :: Monad m => [m a] -> m ()
sequence_ merely takes a list of monadic values and combines them into a single monadic value, discarding any of the elements' return values and returning () instead.

So if everything is a pure function, how do we actually perform the side effects? The simplest way to think of it is that our whole program is a bunch of pure functions which construct a single value representing all of the side effects that will take place over the lifetime of the program. This single value is called main:

    main :: IO ()
    main = sequence_ (map print ["hello","world","!"])
With this idea in mind, we can think of Haskell's runtime as taking this single value main and performing the side effects specified throughout our program.


Actually, every function in Haskell is pure. It's just that some of those pure functions produce values of type (IO a) representing IO actions and, if you sequence those actions into the main action (or a subthread's action), those actions will be performed by the runtime.

So when people say that some functions in Haskell are "impure" they mean that they produce IO actions that, if sequenced, will depend upon or cause IO effects. Thus, both

    map print ["hello","world","!"] :: [IO ()]
and

    mapM_ print ["hello","world","!"] :: IO ()
are equally pure or impure: They both produce actions that have side effects if sequenced. It's just that the first must be sequenced differently than the second since it produces a list of actions and not a singleton action. Since singletons can be sequenced with (>>) and (>>=) you can insert them directly into do notation, which makes many people believe that they are somehow different in terms of purity. (But they are not.)


That's what I said.


I wasn't contradicting you but trying to reinforce the point that there's an equivalence between "impure" functions and pure functions that produce actions (having impure effects if sequenced). In particular, I wanted to highlight that (mapM_ print) is not somehow more impure than (map print). Many people seem to believe that it is.


Indeed. The source code of mapM_ is literally:

   mapM_ f as      =  sequence_ (map f as)


map print doesn't have side effects. mapM_ print, certainly.


(mapM_ print) produces a pure function that produces a single IO action, and (map print) produces a pure function that produces a list of IO actions. Neither has any effect unless called in a context that sequences the resultant actions into the main IO action that the runtime interprets (or a thread's action).


I meant mapM_ in an IO action actually executed. On the other hand, getting a list of IO actions, even in the IO monad, doesn't amount to much.


And my point is that the action that mapM_ ultimately produces is not actually executed unless you sequence it into the main action (or a thread's action). Since mapM_'s eventual action is a singleton, you can do this sequencing with any combinator taking a singleton action, for example (>>) or (>>=), but it must be sequenced nonetheless, the same as for the list of actions that (map print) produces, if you want those actions to be executed.


Fair enough.


What benefit does that have over what's actually there?


My point was not to argue that something like that would be better solution, but since you're asking: Having a special syntax would make the learning curve a little shallower for newcomers. And it would simplify certain constructs -- instead of having to lift IO values or using mapM_ or whatever, you could actually deal with the results from impure functions directly, no unwrapping or rewrapping needed.

While using the type system to implement an effects system is theoretically elegant, I think it's a beautiful hack that has made the language fussier and more obtuse in practice.


That would certainly be true if purity enforced by monadic I/O were the end of the story, but it isn't. While new users create a lot of hot air about monads and I/O, intermediate-experienced Haskell users just use them for various different purposes and get on with life.

At the end of the day most of us have differing opinions on what constitutes simplicity and elegance. It's certainly true that a "pure" annotation like you're proposing is a much smaller change to introduce in an imperative setting. I recollect D or Rust or something is doing this. But in the functional programming context the monadic solution is more general, and a two-function type class with 3 (IIRC) algebraic laws is not considered an overbearing amount of complexity, though there are of course interesting alternatives with their own merits.


Sure. IO plays a part of a larger system of monads and functional programming, but it's not a prerequisite for impurity to exist -- it is more like a happy confluence of various strands of functional theory. After all, Haskell had I/O before the IO monad existed (although it was apparently not a happy solution).

Personally, I find monadic I/O theoretically elegant, but it comes at the cost of clumsiness when applied to real-world programs. To me, Haskell's "do" blocks feel like an implicit admission of this clumsiness; they are a crutch to work around the fact that having to constantly wrap and unwrap data is something of a chore.


I guess I just don't see them that way. Most of the time my code is in pure land, and I don't avoid "do" when the result is more readable. I think the ugliest thing in Haskell is probably monad transformer stacks, but that's mainly because I think they're overused by folks who create more abstraction than they need as a matter of habit.

That said, the kinds of things I do on the side with Haskell tend to have a small, well-defined I/O surface, so it could be that I'm spared the worst of it by my interests. I suppose if that weren't the case I'd probably favor OCaml more than I do.


Rust used to have a "pure" annotation, but it's gone, partially because it was a pain to have to write the annotation everywhere, partially because it's not needed for memory safety anymore, partially because nobody can agree on what "pure" means.


I wouldn't have anticipated that, but now that you say it I could see why that would lead to a debate. How would you deal with mutable data structures, for instance? What if it accesses the environment, but in a fashion you could somehow guarantee were safe? In Haskell the programmer can circumvent the system with unsafePerformIO if they know something they can't convince Haskell otherwise, but it almost seems like you'd need a "pure-but-not-really" annotation to do this kind of thing in an imperative language that actually enforced purity.


C++ walked in these very same footsteps. First by not having const, then by having it, then by allowing exceptions to constness, then by introducing const_cast and finally by allowing temporarily mutable const objects.


C++ const is defective because it's a shallow const. You can modify an object through a const pointer.

The D language "fixes" this by making const transitive (and also adding an immutable annotation, which means the object is truely read-only, as in "read-only memory").


"pure nothrow @safe" is what you get to prepend to your functions in D. I prefer Haskell's approach.


And how would you achieve that in practice?


I like the uniqueness typing approach in Clean very much.

Function can manage state of variable destructively if the variable is declared unique (there can't be other references, so there can't be side effects from destructively modifying the value).

It's easy to understand. It opens more avenues for fast code, and it keeps the purity.


Have you actually used Clean in production code? Can you share some of the tradeoffs of that approach?

Looking at Clean's documentation it certainly seems nice, but I know of no other language that had adopted Uniqueness typing.

[Clojure's Transients (http://clojure.org/transients) perhaps?]


> Thus the lack of IO in a function's type assures you, reliably, that the function does not cause side effects

No. IO is not the only monad encoding side effects.


Indeed. To be precise, I should have written that the lack of X in a function's type assures you that the function does not cause X effects.


How it so significantly better than using, say, Erlang or CL with a strict self-discipline (separation of impure functions, using appropriate naming conventions, etc.?


Discipline is great, but it's finite and has to come not just from you, but from everyone else on your team as well.

The compiler remains vigilant and uncompromising forever.

It's best to automate everything you can, and ask your team for discipline only as a last resort.


OK, let's say that it is better first to learn how to make trees with conses and traverse them using maps and folds with null? as a base case, and then enjoy a compiler which won't allow you to "cons" improper element to it.)


Why is this better? I see no obvious reason. Just Cons Nothing values.


Monads don't really have much to do with IO. It's up in the air whether IO really even is a monad. See Conal Elliot's answer on SO and the link he provides: http://stackoverflow.com/a/16444789/65799


You mean...all this time... !

Reminds me of the story of the old monk who emerges from the basement of the monestary holding an ancient parchment. Tears are streaming down his face. The student asks "What's wrong?" The old monk replies "All this time! We were supposed to be celebRate!"


A thing that people are often unaware of when trying to understand how the IO type works in Haskell is that you cannot define the IO type in Haskell.


Sure you can: http://hackage.haskell.org/packages/archive/ghc-prim/0.2.0.0...

You can't define RealWorld, though, nor (IIRC) is IO really a state monad.


I used "define" imprecisely.

You cannot, so far as I know, implement `>>=` for the IO type defined in the above link within Haskell, which means you can't actually use it to do IO.



Here's another definition -- by continuation -- from Hug's Prelude (http://cpansearch.perl.org/src/AUTRIJUS/Language-Haskell-0.0...) :

    newtype IO a = IO ((a -> IOResult) -> IOResult)

    data IOResult 
      = Hugs_ExitWith    Int
      | Hugs_Catch       IOResult (Exception -> IOResult) (Obj -> IOResult)
      | Hugs_ForkThread  IOResult IOResult
      | Hugs_DeadThread
      | Hugs_YieldThread IOResult
      | Hugs_Return      Obj
      | Hugs_BlockThread (Obj -> IOResult) ((Obj -> IOResult) -> IOResult)


I just assumed IO meant mutation.


I don't agree that managing effects is over the top per se however the use of monads feels over the top for pretty much everything!

It's always struck me as strange that value (as in a typical type system) and effects would be controlled through the same system. The type signature of a function and it's effects seem very much orthogonal to me.

If we want to be controlling side effects then we really ought to be using a separate effect system[1]. There's a scala plugin demonstrating this (although I've not tried it)[2].

With separate type and effect systems I should be able to define a pure function fib(n) and call it like this fib(getValueFromUser()) that is without having to use special operators to get at the value which can only be used in certain contexts a la Haskell.

[1] http://en.wikipedia.org/wiki/Effect_system

[2] https://github.com/lrytz/efftp/wiki


I think a separate effect system would be needless specialisation. The IO monad has shown that effects can be reasonably well encoded in the existing type system, the problem is just that composing lots of effects starts to get complicated. I think any solution should involve extending the type system in a general way, or even just providing some kind of syntactic sugar to hide the noise from more complicated types.


For Haskell you're absolutely right however I was responding to the OPs comment that controlling effects in general is over the top.

The issue of composing effects, along with the widespread fear of monads, are enough for me to conclude that monads are not the best way to control effects.

I don't remember ever seeing any solutions other than monads and deprecate effect systems, hence my preference for the later.


The progress that has been made in Haskell in the past decade leads me to believe that the issues with the current system can be solved with better abstractions, and possibly new syntax. Or if the solution isn't possible in Haskell, that it will be born of a similar philosophy.

I don't yet know whether fear of monads is something the programming community will grow out of, or whether there is a more fundamental reason people have difficulty with them. From what I've seen, the response of most people on finally "getting" monads is "Wait that's it?", so I'm inclined to believe a significant part of the hurdle is the fear itself.


    x = fib(getValueFromUser())
x is not pure, but fib is. OK, the compiler can figure that out without requiring the programmer to write a special "bind" operator.

How about this:

    dofib(argumentProvider) = fib(argumentProvider())
    dofib(lambda: 1)  // pure
    difib(getValueFromUser) // effectful
Is dofib pure or not? That depends on the value of argumentProvider cannot be determined statically.


Is dofib pure or not?

Food for thought:

1. The easy but limited solution is that this code doesn't compile, because argumentProvider must have a single type/effect and you couldn't have both a pure and an impure function with that type/effect.

2. Is purity that important? Ultimately we care about avoiding our programs doing unintended things, and often effects are just fine as long as they don't misbehave in some way. Purity is a means to an end.

3. It's fascinating to extend the ideas of generic programming from mainstream type systems to effect systems. I suspect there is a lot of potential benefit to be had if we can figure out how to do this without introducing a lot of boilerplate code, in the same way that we can write code using generic types to various degrees today but have type inference spare us a lot of keyboard bashing.


Effect inference has already been done: http://research.microsoft.com/apps/mobile/showpage.aspx?page...

I doubt we'll see anything like this in a mainstream language anytime soon.


That's an interesting case I hadn't seen before, so thanks for the link.

Some of the related pages don't seem to be available at the moment, but from what I could see it looks as if that approach still gets hung up on questions of decidability, and doesn't have a very powerful concept of the regions where effects apply, which has been another interesting aspect of the wider research so far. It's good to see someone else working on the field, though.


Using the mechanic employed by the scala plugin I linked to[1], dofib would be annotated with pure(argumentProvider). dofib itself is pure however, at each call site, it has the effect of its argument for that call. This is consistent with your example.

[1] https://github.com/lrytz/efftp/wiki/Relative-Effects

edit: That's effectively (sorry!) higher-order effects, behaving just as you'd expect.


dofib is a pure, higher order function. dofib(getValueFromUser) always returns the same (non-pure) function, but it is always the same. If the argument is provided by another variable, then you need to look up the chain, and would likely have the type/effect system force you not to mix pure/impure functions in the possible parameters, or if you do mix them, call dofib(f) impure.


Well, if you're working in a proper language with an effect system, then dofib is parametrically effect-polymorphic on the effect of its input function.


An effect system cannot express what transformers can. Consider the difference between StateT s Maybe and MaybeT (State s).


LINQ (C#) seems pretty awesome and not over-the-top, and I believe it's foundation is monad-based. Best example I can think of.


I'm having fantastic success with a custom IO-like monad that lets me statically verify that my unit test suite is totally side effect free.

I don't have to resort to documentation or code reviews to ensure that my teammates write fast, reliable tests. This isn't possible in a language that doesn't restrict side effects.


Tmoertel, your comment is dead for some reason and you might want to talk to the admins.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: