runarorama / runarorama.github.com

9 stars 3 forks source link

(post comments) "What purity is and isn't" #3

Open runarorama opened 11 years ago

runarorama commented 11 years ago

This issue is reserved for comments on the blog post What purity is and isn't. Leave a comment below and it will show up on the blog post's Web page. Thanks!

ouertani commented 11 years ago

thanks for this post ! below another example about side effect and pure function : int n =2 int inc(int k) { n = n+k; return n;}

=> where inc(1) + inc (1) != 2 * inc(1)

peregin55 commented 11 years ago

One aspect that I struggle with is debug logging, for instance logging the arguments in an otherwise completely pure function. The logging doesn't change the answer nor the execution of the program in any way. But it's useful when tracking down a bug to know that at a given point in time a particular function was given a specific piece of data.

runarorama commented 11 years ago

peregin55: Introducing a side-effect can be a useful quick-and-dirty debugging technique in some languages. But you should be wary of the fact that it either relies on or changes the strictness of the arguments. And to be honest, debug logging is a hack to work around the fact that your debugger doesn't have adequate tracing or observation facilities. Think about it; it's kind of ridiculous that you would have to change the code of a function in order to make runtime observations.

john-kurkowski commented 11 years ago

Thanks for the write up. In my FP nascence, the enlightenment about purity comes down to "delimiting such behavior in a specific way." Nice.

It's tough sharing this with my fellow learners though, because you say your examples are "clearly" referentially transparent, while it took me a good amount of time to stare at the snippets, and some broader assumptions about what readLine and stdin were.

In the longer term, in a bigger project, maybe that'd be a good lesson though! At a glance, can one say what parts of the project are pure? It was hard staring at these snippets, but less so if they visibly applied types like IO or called unsafePerformIO.

luqui commented 11 years ago

I guess I fall into the rationalist camp. I agree with you about purity being defined relative to some context -- hence whether code is "pure" is not an objective statement without some more information. This leads to arguments such as C is purely functional, which, knowing Conal, is an argument that computations in Haskell's IO monad are not actually pure, as often claimed. (His and my position, I believe, are more nuanced than that, but that's approximately it).

And in Haskell, among the purist of languages, there are still effects. Nontermination is an effect. Even currying is an effect, from some perspectives. Purity is a fuzzy term, and I don't think it is awfully useful unless you are talking with others who you know share your definition. I like denotations, and I would like to talk about the complexity of the semantic domain rather than a true-or-false pure judgment. E.g. C's semantic domain is more complicated than Haskell's. (Of course, complexity is also subjective -- some say Haskell is more complicated than C because they have a model of how a computer works which C matches more closely than Haskell)

chris-martin commented 11 years ago

Hope you don't mind, I put your code into a Gist so I could play with it (and it's hard to copy code off of the blog page):

https://gist.github.com/chris-martin/5229048/291cfb7669bbe8ab71713e146bf11344c5105b7e

I don't understand quite what you were trying to express with eval2, because it doesn't compile.

runarorama commented 11 years ago

Thanks! The code is fixed now.

jbee commented 11 years ago

Hi. I read "Purly Functional IO" (slides). While I understand the Haskell and Scala examples in principle I think the way of handling or formalising it is not very intuitive and cumbersome. My interest goes back to a more fundamental conceptual point. You say

Instead of performing I/O as a side-effect, return a value to the caller that describes an interaction with the external system.

Interaction is clearly very open. In case of an IO stream, reading a value would describe itself how ?

read :: Stream -> (Stream, Value)

Does it return a tuple of the stream after we have read the value together with the value ? What about error cases ? Doesn't what imply what we could read from whatever earlier position in that stream as long as we have the earlier stream object at hand somewhere ? I don't see how a stream could successful be implemented that is both a stream and allows to be used like that. So I think I miss something here or is the basic idea to produce successors of a sink or source instead of mutating it in place ? This would be pure from the viewpoint of our program to some extend, but as I tried to sketch out I can't see how to fully grant it.

runarorama commented 11 years ago

Yeah, in the world-as-state model, the action of type S -> (S, A) returns the modified S in the tuple. The original S is understood to be discarded and so it is mutated in place. It's safe to do that as long as nobody holds on to the previous S. In practise, the world state is represented by nothing at all, so even if somebody held on to it, they can't inspect it in any way.

jbee commented 11 years ago

Hm.. and is there another pure model I am missing now ? Disallowing to actually use old state isn't so pure to me. It means I cannot do the same thing on the same value. So calling same function with same state argument would actually not result in same thing as it would throw some form of exception at me or similar because I had used old state. I don't say its bad to not allow to further use old state just that to me this is a different contract than the pure one. It is a more limited one and maybe we do well to limit ourselves as much as possible when dealing with state but lets use another term for it since it appears not to be the same animal.

runarorama commented 11 years ago

The "world" state is not accessible to your program at all. It's created outside of your program and passed in. All your program can do is pass it along. It's a kind of token and it's totally opaque. If you pass it to a function, it doesn't matter because that function cannot do anything with it.

The ST monad models this explicitly. An ST action has the type S -> (S, A) where S is a "state thread" token. If you have a token of type S, then any mutable state marked with the same type S is safe to mutate and this is guaranteed by the type system:

runST :: (forall s. ST s a) -> a

This means your ST action has to be able to take any s. In other words, it cannot look at it.

The traditional IO monad can be thought of as ST RealWorld, where RealWorld in practice is represented by null.

jbee commented 11 years ago

Can you agree to this proposition?

The "trick" of pure IO is to make sure (via type system) that a state value can be used just to produce its successor state value once (along with 0 or more other function results).

That implies any of this state values cannot be observed nor inspected in any way other than producing successors of themselves along with further values (that are dependent on the state value they have been derived from).

runarorama commented 11 years ago

That sounds roughly right, although the type system doesn't really make this guarantee in the IO type. It does in the ST type though. The trick with pure IO is really that nobody can create the "realworld" state token except the IO manager itself, and that its structure is totally opaque.

What you describe is much more like the situation with uniqueness types, as in the Clean programming language. There, the referential transparency of IO actions is enforced by the fact that the type of every mutable object (including things like file handles and output streams) is guaranteed to be unique. So you cannot call the same function twice on the same mutable argument, since that would violate the uniqueness typing.