Where did the structure go?

g :: Applicative f => (t -> a) -> t -> f a
g f x = pure (f x)
ghci> g (+1) 1
2

The result should be `f a` but the structure is not visible in GHCi. Wondering if it is still there?

I’m guessing that f defaulting to IO in ghci.

3 Likes
> :t 2
2 :: Num p => p

Is 2 an Applicative because it is Num?

No, 2 is not an Applicative, but IO is, as @gilmi is hinting to.

In ghci, everything is considered in the IO monad (roughly speaking).

For example

ghci> pure 1 :: IO Int
1

and

ghci> getLine :: IO String
test <-- Input
"test" <-- Output

In your case it’s simply getting cast to IO, here illustrated using replacement:

ghci> g (+1) 1 :: IO Int
2
ghci> pure ((+1) 1) :: IO Int
2

My question really came from expecting to see some indication of structure as this does

ghci> (+) <$> Just 1 <*> Just 1
Just 2

Perhaps that is nonsensical since Maybe is a type and Applicative is a class. I have the sense that I’m missing something obvious. Maybe it is that a class is a context and a type is a context but I need to think through the differences.

So in ghci (but not if you write out the program in a text file and compile it), things of type IO a tend to get handled specially. For example, if I have a :: IO Int and I enter a in ghci, it will print out an Int.

Try doing :t g (+1) 1 in order to see the type of the expression. That should help work out what’s going on.

Having seen some other examples of confusion caused by this “default IO context” in GHCi, maybe there’s a need for an "educational/introductory" mode or option, which means e.g. GHCi uses an empty context like GHC does for compiled modules (cf. the principle of least surprise astonishment), until new Haskellers are sufficiently annoyed by having to type in something like:

ghci> :#getLine
test
"test"
ghci>

…just to run an IO action in GHCi.

It’s worth noting that if you pick a different Applicative besides the default IO, you’ll get results that you expect:

g :: Applicative f => (t -> a) -> t -> f a
g f x = pure (f x)
Prelude> g (+1) 1 :: Maybe Int
Just 2

I noticed that. So is that saying that if the context is the default GHCi will not show it but otherwise will?

Yes, or more specifically if the context is IO (which is the default in GHCi), then GHCi will not show it. And that’s because GHCi has custom behavior for handling of IO. Outside of GHCi, there won’t be any weirdness, and inside GHCi the weirdness only happens if it’s specifically IO.

4 Likes

I just tried it and used :t to see if it’s IO, turns out it’s just an unspecified Applicative

λ :{
Prelude| g :: Applicative f => (t -> a) -> t -> f a
Prelude| g f x = pure (f x)
Prelude| :}
Prelude
λ :t g (+1) 1
g (+1) 1 :: (Applicative f, Num a) => f a

I know that doesn’t answer the question why, but at least we know it didn’t pick IO as the type.

Welcome francisco!
While the type of g (+1) 1 might be (Applicative f, Num a) => f a, that type isn’t ready to be executed yet. f and a have to be resolved to concrete types in order to execute it. You can observed this by seeing what the return type is after you evaluate that statement:

Prelude> g (+1) 1
2
Prelude> :t it
it :: Integer

(note that it refers to the result of the previous statement.) So you might expect it :: Num a => a, but that’s not a valid runtime type. It has to choose some concrete type for a, and it chooses Integer by default. Think of Applicative and Num similar to interfaces in an OO language. You can work with just an interface at compile time, but at runtime, it has to be some concrete type like a class that implements the interface.

I can’t find any good way to prove that it chooses IO as the default Applicative in GHCi, but if you read this, it makes sense since IO is the only Applicative said to be given the sort of special treatment that could explain this behavior. To cherry pick some statements:

If you type something of type IO a for some a , then GHCi executes it as an IO-computation.

However, there’s no monad overloading here: statements typed at the prompt must be in the IO monad.

5 Likes