5

I recently saw a simple example that brought <* and *> to light.

validate :: String -> Maybe String
validate s =  if s=="" then Nothing else Just s

>validate "a" *> validate "b"
Just "b"
>validate "" *> validate "b"
Nothing
>validate "a" <* validate "b"
Just "a"
>validate "a" <* validate ""
Nothing
>validate "a" <* validate "b" <* validate "c"      
Just "a"
>validate "a" *> validate "b" <* validate "c"      
Just "b"

This shows that the effects are important even if the values they produce are not.

My question is about the type signatures.

(*>) :: f a -> f b -> f b
(<*) :: f a -> f b -> f a
  • Do these type signatures actually imply the behavior shown above?

I can see how one could reason "Obviously we have an Applicative - so that says something about behavior in general. For operator *>, since we are throwing away the left value, the only possible meaning this function could have would be how the effect of the left hand side affects the entire operation."

In the case of Maybe - then it seems that yes - the behavior is implied. For Either, likewise the implication holds and the error would be propagated on an effect 'failure'.

Note I can only say the above because I now know how the implementation works, where my question pertains to the seasoned functional programmer who sees a signature like this the first time.

I have read where a type signature like [a] -> b :: Int (probably not real code there) all but implies the implementation as the length of the list.

I have also searched for "implying implementation from type signature" and found that people have-been/are working on such things - but in general cannot be done (Uh - without that new GitHub thing :--)

So perhaps I have answered my own question but would appreciate any other answers or comments. I am still new to Haskell and after a number of false starts over the years, it is finally starting to sink in. And I have only scratched the surface...

Thanks

Will Ness
  • 70,110
  • 9
  • 98
  • 181
user49011
  • 523
  • 1
  • 3
  • 10
  • As @WillemVanOnsem alludes to, `<*` and `*>` are supposed to be `liftA2 const` and `liftA2 (flip const)` respectively. The conditions specified in [the `Control.Applicative` docs](https://hackage.haskell.org/package/base-4.16.0.0/docs/Control-Applicative.html) amount to that. – duplode Dec 10 '21 at 13:31
  • @duplode I think the allusion was more to the "wrong" implementations of `const` and `const id`. – Will Ness Dec 10 '21 at 13:57
  • @WillNess It was to both: Willem's comment mentioned the wrong implementations, and that we need laws to rule them out. – duplode Dec 10 '21 at 14:04
  • The [`Control.Applicative.Backwards.Backwards`](https://hackage.haskell.org/package/transformers-0.6.0.2/docs/Control-Applicative-Backwards.html#t:Backwards) wrapper gives you the opposite order (e.g. `Backwards fa *> Backwards fb` = `Backwards (fb <* fa)`), like [`Data.Monoid.Dual`](https://hackage.haskell.org/package/base-4.16.0.0/docs/Data-Monoid.html#t:Dual) does for `(<>)` and [`Data.Ord.Down`](https://hackage.haskell.org/package/base-4.16.0.0/docs/Data-Ord.html#t:Down) for `compare`. – Jon Purdy Dec 10 '21 at 22:17

1 Answers1

9
(*>) :: f a -> f b -> f b
(<*) :: f a -> f b -> f a

Do these type signatures actually imply the behavior shown above?

Of course not. The two operations could be implemented simply as

apR :: (Applicative f) => f a -> f b -> f b
apR a b = b

apL :: (Applicative f) => f a -> f b -> f a
apL a b = a

just ignoring the Applicative constraint on f.

And in the same way [a] -> Int does not imply the implementation being length. It could just as well be

foo :: [a] -> Int
foo _ = 42

But in both cases these are not the "right" implementations in the sense that the inferred types are different from the signatures given.

Your question is then, perhaps, a bit different, like, suppose there is an implementation with the matching inferred type. Does it follow that it does the thing we intended?

The answer seems to still be no. For one, we can define

apR2 :: (Applicative f) => f a -> f b -> f b
apR2 a b = pure (\a _ b -> b) <*> a <*> b <*> b

For some types, like your examples, it won't make a difference. But in general, doing the effects twice is different than doing them only once.

Another, even more meaningfully "wrong" implementation (with thanks to Daniel Wagner for the comments), is

apR2b :: (Applicative f) => f a -> f b -> f b
apR2b a b = pure (\b a -> b) <*> b <*> a

Now even your examples won't always work, because the order of effects is different -- it "does" b's first, before the a's.

Will Ness
  • 70,110
  • 9
  • 98
  • 181
  • 2
    There's not just how many times the effects are done, but also what order. For example, `foo fa fb = pure const <*> fb <*> fa` also has the same inferred type as `(*>)`, executes all the effects only once, and is still meaningfully different from it. – Daniel Wagner Dec 10 '21 at 17:18