2

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?

Tshimanga
  • 845
  • 6
  • 16
  • Further, despite my referring to it as `(<-)` throughout the question; you can't actually use it as a prefix operator like that... – Tshimanga Mar 23 '17 at 20:03
  • 6
    `<-` is not a function at all, it is a part of Haskell’s *syntax*. Specifically, it is specially recognized by the parser as part of `do` blocks. – Alexis King Mar 23 '17 at 20:14
  • As Alexis King says `(<-)` is not a function; it is just syntax sugar. In `do { x <- m; f x }`, you can't meaningfully separate the `<-` from the `;`. – duplode Mar 23 '17 at 20:15

3 Answers3

9

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.

jberryman
  • 16,334
  • 5
  • 42
  • 83
  • Indeed, so if I want to do without that symbol, is `Comonad` the only/best way or are there other/better ways? – Tshimanga Mar 23 '17 at 20:19
  • 4
    @Tshimanga `do` notation is *just sugar*. You can rewrite *any* `do` block using `>>=`, `>>`, lambdas, and `let`. You could use the unsugared syntax if you’d like. – Alexis King Mar 23 '17 at 20:21
  • @AlexisKing, then what does `x <- a` desugar to? I'm inclined to believe @jberryman and @duplode in that, on it's own, it doesn't actually represent a proper expression outside of the GHCi execution enviornment. I have tried `(connect $ def {creds}) >>= (\pipe ->return pipe)` and the line runs in GHCi without error, but it tosses the resulting `m Pipe` (which isn't what I want anyways) out into the ether without a handle haha – Tshimanga Mar 23 '17 at 20:31
  • GHCi operates in IO, which is why you can do stuff like `print 123` and it prints 123. So you can use `str <- getLine`, and it'll wait for you to put in a line, and then it'll bind `str` to the result. – ephrion Mar 23 '17 at 20:41
  • 2
    @Tshimanga and `Comonad` has nothing to do with this. You'd be better off forgetting comonads are even a thing until long after you've completely understood monads (and applicatives, and transformers, and lenses, and categories, and...) – Benjamin Hodgson Mar 23 '17 at 20:54
  • 1
    @BenjaminHodgson I only suggested that `extract` would be an alternative method of unpackaging monads that doesn't really on the syntactic sugar of `do`-notation. I'm by no means married to the idea of making heavy use of `Comonad`, nor am I suggesting a deeper connection than "this also does the job". If you have a third alternative (that is, neither sugar nor `Comonad`) to offer, I'm always open to learning something new – Tshimanga Mar 23 '17 at 21:04
  • 2
    @Tshimanga `x <- a` cannot desugar to anything in isolation because `x <- a` is only valid inside a larger `do` block. It would be like asking what `(3 +` means—it is not a valid expression. However, *any* valid `do` block **can** be converted to code without `do` notation. For example, `do { x <- a; return x }` is desugared to `a >>= (\x -> return x)` (though that is redundant, since it’s precisely equivalent to `a` by the monad laws). – Alexis King Mar 23 '17 at 21:08
  • @AlexisKing indeed, and happens to be my point in the last comment I directed to you. All that said, while saying that it "desugars" to it certainly seems a touch too strong a statement, `extract` would seem to offer a suitable alternative in accomplishing the same task. What do you think? – Tshimanga Mar 23 '17 at 21:16
  • 1
    @Tshimanga But it ***does*** desugar to that. What you seem to be misunderstanding is that is **literally** how GHC compiles `do` blocks. They get converted to uses of `>>=` and `>>` as a compilation step, I believe before GHC generates core. `do` blocks are macros, they are syntactic sugar, they are shorthand syntax, and they specifically work with monads (`RebindableSyntax` aside). Saying “desugars” is too strong is not true, since it ***literally does*** desugar. – Alexis King Mar 23 '17 at 21:31
  • @AlexisKing, i meant too strong to say it desugars to `extract`. I'm aware of how `do` blocks are translated into the natural transformations by the compiler. Initially, my curiosity was about what I should probably call an incomplete segment of a `do` block (GHCi was completing it for me). Now I just want to explore other ways of unpacking monads – Tshimanga Mar 23 '17 at 21:44
  • 3
    @Tshimanga I think we all misinterpreted your question a little. What might be helpful is to observe that `a <- x` doesn't actually "extract" a value. What `Monad` gives you is _composition_ (`(>>=) :: Monad m => m a -> (a -> m b) -> m b`); we can never pull a value of type `a` or `b` out of one of those `m a` or `m b` (except for certain _concrete_ `m`); indeed this "limitation" is what allows haskell to use monads to accomplish `IO` while maintaining purity. All this is what I think @BenjaminHodgson is getting at when he suggests you have more studying to do to fully grok Monad – jberryman Mar 23 '17 at 21:44
  • 1
    More concretely, most things that are instances of `Monad` have no possible law-abiding `Comonad` instance, so you can't expect to use `extract` on them. – jberryman Mar 23 '17 at 21:47
  • 2
    @jberryman yeah I think my initial confusion stemmed from that when I typed `a <- x` in GHCi, it actually returned to me an unpacked pipe. This made me think that in general `a <- x` did some unpacking, despite knowing that the natural transformations of a monad can't actually do that. Maybe I thought there was some caveat in Haskell that made things not quite like the category theoretic concepts I'm familiar with. – Tshimanga Mar 23 '17 at 21:52
  • @Tshimanga Everything entered into GHCi is effectively inside one big `IO` `do` block, which is why that syntax works in the REPL. When you used `a <- x` in the REPL, you are using the `>>=` operation of `IO`. The type of `connect` has a `MonadIO` constraint. Comonads and `extract` are not being used anywhere. – Alexis King Mar 23 '17 at 22:13
  • @AlexisKing I wasn't making the claim that `Comonad`s or `extract` were lurking under anywhere the hood. Just saying that since `a <- x` *as typed into the REPL* isn't a valid expression, I might be better disciplined to use another approach that doesn't depend on GHCi covering the rest. The type signature of `extract` suggested the possibility that I might use that instead to write something that not only executed as desired in the REPL but also represent valid code on it's own *as written*. – Tshimanga Mar 23 '17 at 22:23
  • @AlexisKing again, I never was making the claim that there was a fundamental link between `do` and `Comonad`. Merely entertaining the idea of whether or not there could be an opportunistic way to implement `extract` to do the same process in this specific situation. – Tshimanga Mar 23 '17 at 22:26
4

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)

Lazersmoke
  • 1,741
  • 10
  • 16
  • yes, i know. im not trying to create `comonad` instances for `monad`s in general. that'd be a silly short-lived endeavour. i was entertaining whether there it was possible to create a `comonad` instance to leverage for this specific scenario in particular. to my knowledge, while it's true that **most** `monad`s don't support a `comonad`, I have not encountered the statement that **no** `monad`s can implement `comonad` instances – Tshimanga Mar 23 '17 at 22:33
2

So what is (<-) exactly? Can it be replaced entirely by using extract? 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 Integers 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.

Community
  • 1
  • 1
duplode
  • 33,731
  • 7
  • 79
  • 150