Reputation: 885
So I'm playing around with the hasbolt module in GHCi and I had a curiosity about some desugaring. I've been connecting to a Neo4j database by creating a pipe as follows
ghci> pipe <- connect $ def {credentials}
and that works just fine. However, I'm wondering what the type of the (<-)
operator is (GHCi won't tell me). Most desugaring explanations describe that
do x <- a
return x
desugars to
a >>= (\x -> return x)
but what about just the line x <- a
?
It doesn't help me to add in the return
because I want pipe :: Pipe
not pipe :: Control.Monad.IO.Class.MonadIO m => m Pipe
, but (>>=) :: Monad m => m a -> (a -> m b) -> m b
so trying to desugar using bind
and return
/pure
doesn't work without it.
Ideally it seems like it'd be best to just make a Comonad
instance to enable using extract :: Monad m => m a -> a
as pipe = extract $ connect $ def {creds}
but it bugs me that I don't understand (<-)
.
Another oddity is that, treating (<-)
as haskell function, it's first argument is an out-of-scope variable, but that wouldn't mean that
(<-) :: a -> m b -> b
because not just anything can be used as a free variable. For instance, you couldn't bind the pipe to a Num
type or a Bool
. The variable has to be a "String"ish thing, except it never is actually a String
; and you definitely can't try actually binding to a String
. So it seems as if it isn't a haskell function in the usual sense (unless there is a class of functions that take values from the free variable namespace... unlikely). So what is (<-)
exactly? Can it be replaced entirely by using extract
? Is that the best way to desugar/circumvent it?
Upvotes: 2
Views: 314
Reputation: 34378
So what is
(<-)
exactly? Can it be replaced entirely by usingextract
? Is that the best way to desugar/circumvent it?
Note that the do-block <-
and extract
mean very different things even for types that have both Monad
and Comonad
instances. For instance, consider non-empty lists. They have instances of both Monad
(which is very much like the usual one for lists) and Comonad
(with extend
/=>>
applying a function to all suffixes of the list). If we write a do-block such as...
import qualified Data.List.NonEmpty as N
import Data.List.NonEmpty (NonEmpty(..))
import Data.Function ((&))
alternating :: NonEmpty Integer
alternating = do
x <- N.fromList [1..6]
-x :| [x]
... the x
in x <- N.fromList [1..6]
stands for the elements of the non-empty list; however, this x
must be used to build a new list (or, more generally, to set up a new monadic computation). That, as others have explained, reflects how do-notation is desugared. It becomes easier to see if we make the desugared code look like the original one:
alternating :: NonEmpty Integer
alternating =
N.fromList [1..6] >>= \x ->
-x :| [x]
GHCi> alternating
-1 :| [1,-2,2,-3,3,-4,4,-5,5,-6,6]
The lines below x <- N.fromList [1..6]
in the do-block amount to the body of a lambda. x <-
in isolation is therefore akin to a lambda without body, which is not a meaningful thing.
Another important thing to note is that x
in the do-block above does not correspond to any one single Integer
, but rather to all Integer
s in the list. That already gives away that <-
does not correspond to an extraction function. (With other monads, the x
might even correspond to no values at all, as in x <- Nothing
or x <- []
. See also Lazersmoke's answer.)
On the other hand, extract
does extract a single value, with no ifs or buts...
GHCi> extract (N.fromList [1..6])
1
... however, it is really a single value: the tail of the list is discarded. If we want to use the suffixes of the list, we need extend
/(=>>)
...
GHCi> N.fromList [1..6] =>> product =>> sum
1956 :| [1236,516,156,36,6]
If we had a co-do-notation for comonads (cf. this package and the links therein), the example above might get rewritten as something in the vein of:
-- codo introduces a function: x & f = f x
N.fromList [1..6] & codo xs -> do
ys <- product xs
sum ys
The statements would correspond to plain values; the bound variables (xs
and ys
), to comonadic values (in this case, to list suffixes). That is exactly the opposite of what we have with monadic do-blocks. All in all, as far as your question is concerned, switching to comonads just swaps which things we can't refer to outside of the context of a computation.
Upvotes: 2
Reputation: 1741
Ideally it seems like it'd be best to just make a Comonad instance to enable using extract :: Monad m => m a -> a as pipe = extract $ connect $ def {creds} but it bugs me that I don't understand (<-).
Comonad has nothing to do with Monads. In fact, most Monads don't have any valid Comonad instance. Consider the []
Monad:
instance Monad [a] where
return x = [x]
xs >>= f = concat (map f xs)
If we try to write a Comonad instance, we can't define extract :: m a -> a
instance Comonad [a] where
extract (x:_) = x
extract [] = ???
This tells us something interesting about Monads, namely that we can't write a general function with the type Monad m => m a -> a
. In other words, we can't "extract" a value from a Monad without additional knowledge about it.
So how does the do-notation syntax do {x <- [1,2,3]; return [x,x]}
work?
Since <-
is actually just syntax sugar, just like how [1,2,3]
actually means 1 : 2 : 3 : []
, the above expression actually means [1,2,3] >>= (\x -> return [x,x])
, which in turn evaluates to concat (map (\x -> [[x,x]]) [1,2,3]))
, which comes out to [1,1,2,2,3,3]
.
Notice how the arrow transformed into a >>=
and a lambda. This uses only built-in (in the typeclass) Monad functions, so it works for any Monad in general.
We can pretend to extract a value by using (>>=) :: Monad m => m a -> (a -> m b) -> m b
and working with the "extracted" a
inside the function we provide, like in the lambda in the list example above. However, it is impossible to actually get a value out of a Monad in a generic way, which is why the return type of >>=
is m b
(in the Monad)
Upvotes: 4
Reputation: 16645
I'm wondering what the type of the (<-) operator is ...
<-
doesn't have a type, it's part of the syntax of do
notation, which as you know is converted to sequences of >>=
and return
during a process called desugaring.
but what about just the line x <- a ...?
That's a syntax error in normal haskell code and the compiler would complain. The reason the line:
ghci> pipe <- connect $ def {credentials}
works in ghci is that the repl is a sort of do
block; you can think of each entry as a line in your main
function (it's a bit more hairy than that, but that's a good approximation). That's why you need (until recently) to say let foo = bar
in ghci to declare a binding as well.
Upvotes: 9