Home > OS >  Applicative operators <* and *>, type signature implication
Applicative operators <* and *>, type signature implication

Time:12-11

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

CodePudding user response:

(*>) :: 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.

  • Related