9

What is the meaning of the concept of effect in effectful applicative programming?

For example, which parts of expressions below are the effects?

[(+1)] <*> [2,3]
Just (+1) <*> Nothing
Bakuriu
  • 98,325
  • 22
  • 197
  • 231
sevo
  • 4,559
  • 1
  • 15
  • 31
  • 2
    I doubt these terms are used in the Haskell standard. It might be helpful to hear/read them in the context you have. – Shoe Sep 14 '14 at 20:14
  • Those show typical usage of Applicative Functors in regular pure computations. Maybe looking at them in context of real world events would clarify. – Bartek Banachewicz Sep 14 '14 at 20:32

4 Answers4

8

In the FP world, an effect is any type constructor such as Maybe, [], IO, etc. Effects are not to be confused with side-effects. Intuitively, an effect is an additional property of the value you're computing. Maybe Int means that your program calculates an Int with a failure effect, or [Int] means that your program calculates an Int but with a non-deterministic effect (a non-deterministic result is here modeled as a list of possible results).

Going from here, we have the terms applicative effects and monadic effects, which mean that the said effects have Applicative and Monad instances.

I can't find any authoritative information for this, it's just what I have gleaned in my experience.

Ionuț G. Stan
  • 176,118
  • 18
  • 189
  • 202
  • 1
    `[]` by itself is not an effect, `([], pure::a->[a], (<*>)::[a->b]->[a]->[b])` is an effect, given some implementations of `pure` and `(<*>)`. – Will Ness Sep 15 '14 at 10:24
  • @WillNess why do you call it just an effect and not an _applicative_ effect? In my experience, any type constructor of kind `* -> *` is called an effect, without any mention of `Applicative` or `Monad`. – Ionuț G. Stan Sep 15 '14 at 10:41
  • "applicative" is implied here. :) I meant "effect" as in the context of this question about applicatives. Without `pure` and `<*>` (or `return` and `>>=`) implementations in mind, what *is* the effect of `[]`? – Will Ness Sep 15 '14 at 10:44
  • 1
    (and I wasn't disagreeing with you, your point about the confusion between effects and side-effects is exactly right). – Will Ness Sep 15 '14 at 10:49
  • @WillNess as far as I'm understanding, it's not `[]` who's effectful, but rather the expression that produces a `[]`. For example the effect of `quux :: a -> [a]` is non-determinism. And there's no mention of `Applicative` or `Monad`. Same for `Maybe`. There's an expression with some effect. – Ionuț G. Stan Sep 15 '14 at 10:49
  • 1
    @WillNess yes, I understand you're not disagreeing, I just want to clarify my thoughts by means of discussion :) – Ionuț G. Stan Sep 15 '14 at 10:50
  • no, the effect of `quux :: a -> [a]` is zipped application. Or maybe a possible failure. Without a particular `<*>` implementation, you can't disprove me. – Will Ness Sep 15 '14 at 10:51
  • @WillNess not sure why you called that zipped application... but you still called it an effect, right? Anyway, I'd be happy to read any resource that goes into details about these differences. – Ionuț G. Stan Sep 15 '14 at 10:56
  • yes, an effect - a *different* one. Like [`ZipList a`](http://hackage.haskell.org/package/base-4.7.0.1/docs/Control-Applicative.html#t:ZipList) has. *Not* non-determinism. A kind of inner product, not outer product. – Will Ness Sep 15 '14 at 12:13
7

Much confusion was caused by the unfortunate choice of names, as is quite common in Haskell (think "return", much better named "emit").

pure x is not pure, it is x that is pure, pure is just inject. It was envisioned to be used in pure f <*> a <*> b <*> ... pattern, letting us effectfully apply a pure function f.(1)

[] applicative(2) lets us "non-deterministically" apply (<*>, not $) a non-deterministic value (not two values, in your example) to a non-deterministic function; the non-determinism is the effect.(3)

In list applicative, [(+1), (+2)] is a non-deterministic function that might increment a value by 1, and also might increment it by 2. [3,4,5] is a non-deterministic value whose possible values are as listed. Just as we apply normal entities (+1) and 3 normally, with (+1) $ 3 , so can we apply non-deterministic values non-deterministically, with [(+1)] <*> [3] or [(+1),(+2)] <*> [3,4,5].

And with Maybe the possibility of failure is the effect.


(1) as the paper says, in Introduction: "we collect the values of some effectful computations, which we then use as the arguments to a pure function (:)"

(2) [] by itself is not an applicative, ([], pure :: a -> [a], (<*>) :: [a -> b] -> [a] -> [b]) is an applicative, given some (lawful) implementations of pure and (<*>). For the [] functor, there are two possibilities: an outer product-like of the standard [] Applicative instance, and an inner product-like one of the ZipList's.

(3) x is pure (as in, "Haskell is pure"); pure x stands for an effectful computation producing x without actually having any additional effect. "without an effect" refers to the law of pure x *> u == u i.e. pure x doesn't add any effect into the combined computation on top of u's contribution. But the possibility of effect is there. (One example is an idempotent side effect, like triggering an existence of a certain file in a file system: pure () by itself would also perform this effect; (pure id <*>) == id still holds, as it should.)

pure 7 :: IO Int is certainly not the pure (as in, "Haskell is pure") value 7, it is the pure value 7 in an effectful context (here, IO). Even if it does no actual action in that context, it is still in that context (IO).

On the other hand, and unrelated to the purpose of pure, of course any Haskell value is "pure" i.e. referentially transparent. getLine is a pure, referentially transparent Haskell value. It stands for / denotes an effectful I/O computation, getting an input line from a user and producing it as the result to be used by the next I/O computation in a chain.

print 7 is a pure referentially transparent Haskell value. That's not a kind of "pure" that is meant here. [1,2] is a pure value, but seen from another perspective, interpreted by the [] implementation of <*> / pure, it's treated as a nondeterministic value with two possible pure values 1 and 2. Same for [1] = pure 1. It can still be interpreted as a nondeterministic value with one possible pure value, 1.

[1,2] *> [10,20] = [10,20,10,20] ; [1] *> [10,20] = [10,20]. So unlike [1,2], [1] doesn't add any more nondeterminism into the nondeterministic computation described / denoted by [10,20] (under the standard [] Applicative implementation). But it's still a nondeterministic value; it can participate in *>, unlike 1 which can't.

We know a type by kinds of interactions it can participate in.

See also:

Will Ness
  • 70,110
  • 9
  • 98
  • 181
  • I like your explanation for where `pure` got its name. – Michael Steele Sep 16 '14 at 22:53
  • If I undertand @luqui's answer correctly, `pure x` is indeed a pure, referentially transparent value. – sevo Jan 08 '19 at 21:49
  • of course any Haskell value is pure and referentially transparent. `getLine` is a pure, referentially transparent Haskell value. `print 7` is a pure referentially transparent Haskell value. that's not a kind of "pure" that is meant here. `[1,2]` is a pure value, but seen from another angle it's a nondeterministic value with two possible pure values `1` and `2`. Same for `[1]`. It can still be interpreted as a nondeterministic value with one possible pure value, `1`. – Will Ness Jan 09 '19 at 08:24
  • So `pure x` puts a pure `x` into an effectful context without actually performing this effect, So far my intuition of pure was "creates a most minimal context". But I think "creates the possibility of an effect" is more accurate and more natural. And a pure effectful context must not be confused with purity with respect to values. Your comments are very educational and deserve to be part of the answer. –  Jun 06 '20 at 20:21
  • @bob thanks, when I have time. :) should also link that pigworker's do-notation answer, making same distinctions between "values" and "computations" (BTW he's Conor McBride, one of the authors of that Applicative paper, did you know that?). or if you want, you could edit as well... – Will Ness Jun 07 '20 at 05:47
  • @IvenMarquardt I've edited to mention the possibility of an idempotent effect (a trigger of some kind), where `pure ()` would actually have a side effect. – Will Ness Mar 30 '22 at 18:54
5

We could say that an effect of type f a is anything that can't be written as pure x where x :: a.

In the [] applicative, pure x = [x], so [(+1)] = pure (+1) probably shouldn't be considered an effect. Similarly in the Maybe applicative, pure = Just, so Just (+1) is not an effect.

That leaves [2,3] and Nothing as the effects in your respective examples. This makes intuitive sense from the perspective that [] denotes nondeterministic computations: [2,3] nondeterministically chooses between 2 and 3; and the perspective that Maybe denotes failing computations: Nothing fails the computation.

The definition I used that an effect (perhaps "side-effect" would be a better word) is something that can't be written as pure x is just a swing at making your question precise, and does not represent any sort of consensus or standard definition. Will Ness's answer gives a different perspective, that pure generates an effectful computation from a pure value, which has a nice mathematical ring to it -- i.e. this definition would probably be easier to use in precise settings.

Community
  • 1
  • 1
luqui
  • 59,485
  • 12
  • 145
  • 204
  • 2
    `pure x` is not pure, it is `x` that is pure. `pure x` is always an effectful `x`, by definition. `pure (+1) :: [(Num a)=>a->a]` is an effectful increment function - a *non-deterministic* increment function. The implied non-determinism *is* the effect. – Will Ness Sep 15 '14 at 10:35
  • I tried to expound more on what I meant, in my answer. Of course everything depends on our POV: we can see things in terms of `concatMap`, and Haskell itself is fully deterministic of course, but sometimes we prefer to think in terms of non-deterministic application. It is important also not to mix effects and side-effects, as [another answer below](http://stackoverflow.com/a/25838314/849891) points out. – Will Ness Sep 15 '14 at 17:11
  • Very interesting! So, given `pure id <*> v = v` holds, there shouldn't be any side-effect performed by `pure x :: IO a` as well? – sevo Jan 08 '19 at 21:42
  • Proper question: https://stackoverflow.com/questions/54100390/is-pure-x-io-a-a-pure-value-or-one-with-a-side-effect – sevo Jan 08 '19 at 22:03
  • @sevo re your `pure id <*> v = v` question above, maybe for `IO a`, but not in general. I've recently come up with an example of idempotent effect: say, triggering an existence of some file in a file system. then no matter how many times it is done, the effect is the same. with this effect, `(pure id <*>) == id` holds, of course, as it should, but `pure ()` does have a side effect: it creates that file if there's no such file yet. the monad (or applicative) laws only demand that the presence of `pure ()` in the computational chain changes nothing, not that it doesn't "do" anything by itself. – Will Ness Mar 30 '22 at 17:44
4

Effectful applicative programming can be thought of as taking regular non-effectful computations and adding effects to them. These are implemented as Applicative instances. So while Int is a regular value, A Int is an Int with some effect A, and A is an instance of Applicative.

Consider this expression:

x + y :: Int

This expression is not effectful; it only deals with regular, plain values, so to speak. But we can also have effectful addition.

One effect is failure; the computation may fail or succeed. If it fails, then the computation is stopped. This is simply the Maybe type.

Just (+1) <*> Nothing :: Maybe Int

In addition with regular values, you just add the numbers together. But now, we have addition that might fail. So we have to add the numbers together provided that the computation has not failed. We see in this expression that the computation will fail, since the second operand is Nothing.

If your computations can fail for more than simply one reason, you might want to have error messages that report the kind of failure that happened. Then you may use an error effect, which might be represented as something like Either String (String is the type of the error message). The implementation of this Applicative behaves similarly to the Maybe Applicative.

Another example of an effect is parsing. Parsing can be implemented by just using constructors and making them effectful. Say you want to implement a simple arithmetic language with addition and multiplication. This could be your abstract syntax tree (AST):

data Exp = Num Int | Var String
data AST = Add Exp Exp | Multiply Exp Exp

You build up the AST by simply using these constructors. But the problem is that you also need to actually parse the text, so what about the act of parsing? What about keeping track of how much of the text you have consumed? What if the parsing fails, because the text did not conform to your grammar? Well, in libraries like Parsec, that is the parsing effect. You use some Parse data type (that is an instance of Applicative) and lift the constructors into the effectful Parse AST world. Now, you can construct the AST while actually parsing the text, because the parsing is an effect added to the construction of the AST.

Notice that the Parse type was more complicated than both the Maybe and Either String instances; the parser has the effects of keeping track of state like how much of the input text that has been consumed, and a failed parse, which would yield an error message. Applicative effects can be composed together like this.

cmbuckley
  • 40,217
  • 9
  • 77
  • 91
Guildenstern
  • 2,179
  • 1
  • 17
  • 39