Is there any way to take the pain out of expression simplification?
For example, given this expression:
(+) <$> a <*> b $ 1
I would love to see a tool that would explain what it means. It's quite laborious for beginners (finding correct instance function definition in sources, checking operator precedence) to simplify expressions with all steps involved:
fmap (+) a <*> b $ 1
See definition in Data.Functor
(.) (+) a <*> b $ 1
See fmap in Control.Monad.Instances for instance Functor ((->) r)
and so on.
EDIT: To clarify, I'm looking for a way to rewrite expression using actual function definitions so that newcomer could understand the outcome of this expression. How to tell that (<$>) = fmap here? I don't know how to find a particular instance definition (source) using hoogle and other tools.
EDIT: Changed incorrect original expression to match following reductions.
I find that the easy way is to use typed holes available in GHCi 7.8:
> (*10) <$> _a $ 1
Found hole ‘_a’ with type: s0 -> b
Where: ‘s0’ is an ambiguous type variable
‘b’ is a rigid type variable bound by
the inferred type of it :: b at <interactive>:4:1
Relevant bindings include it :: b (bound at <interactive>:4:1)
In the second argument of ‘(<$>)’, namely ‘_a’
In the expression: (* 10) <$> _a
In the expression: (* 10) <$> _a $ 1
So this tells me that a :: s0 -> b. Next is to figure out the order of operators:
> :i (<$>)
(<$>) :: Functor f => (a -> b) -> f a -> f b
infixl 4 <$>
> :i ($)
($) :: (a -> b) -> a -> b
infixr 0 $
So this says that $ is highly right-associative, and given it's type we see that it's first argument must be a function, so a must be a function (double confirmation). This means that (*10) <$> a $ 1 is the same as ((*10) <$> a) $ 1, so we'll focus on (*10) <$> a first.
> :t ((*10) <$>)
((*10) <$>) :: (Num a, Functor f) => f a -> f a
> :t (<$> _a)
Found hole ‘_a’ with type: f a
Where: ‘a’ is a rigid type variable bound by
the inferred type of it :: (a -> b) -> f b at Top level
‘f’ is a rigid type variable bound by
the inferred type of it :: (a -> b) -> f b at Top level
In the second argument of ‘(<$>)’, namely ‘_a’
In the expression: (<$> _a)
So we need a to be a functor. What are available instances?
> :i Functor
class Functor (f :: * -> *) where
fmap :: (a -> b) -> f a -> f b
(<$) :: a -> f b -> f a
-- Defined in ‘GHC.Base’
instance Functor Maybe -- Defined in ‘Data.Maybe’
instance Functor (Either a) -- Defined in ‘Data.Either’
instance Functor ZipList -- Defined in ‘Control.Applicative’
instance Monad m => Functor (WrappedMonad m)
-- Defined in ‘Control.Applicative’
instance Control.Arrow.Arrow a => Functor (WrappedArrow a b)
-- Defined in ‘Control.Applicative’
instance Functor (Const m) -- Defined in ‘Control.Applicative’
instance Functor [] -- Defined in ‘GHC.Base’
instance Functor IO -- Defined in ‘GHC.Base’
instance Functor ((->) r) -- Defined in ‘GHC.Base’
instance Functor ((,) a) -- Defined in ‘GHC.Base’
So (->) r happens to be one, which is awesome because we know a has to be a function. From the Num constraint, we can determine that r must be the same as Num a => a. This means that (*10) <$> a :: Num a => a -> a. From that we then apply 1 to it, and we'd get (*10) <$> a $ 1 :: Num a, where a is some unknown function.
All this is discoverable using GHCi using :t and :i, along with typed holes. Sure, there's a fair number of steps involved, but it never fails when you're trying to break down a complex expression, just look at the types of different sub-expressions.
GHCi was wonderfully and correctly suggested, and I suggest it too.
I also want to suggest Hoogle, because with the instant search enabled (at the top sidebar at the right there's a button for it), you can search for functions very rapidly, it can provide much, much more information than GHCi, and the best part is that you don't have to mention modules to search in them1. This is in contrast to GHCi where you have to import first:
ghci> :t pure
<interactive>:1:1: Not in scope: ‘pure’
ghci> :m +Control.Applicative
ghci> :t pure
pure :: Applicative f => a -> f a
The Hoogle link above is just one (from the Haskell.org site). Hoogle is a program which you can also install on your machine (cabal install hoogle) and execute queries from the command-line (hoogle your-query).
Sidenote: you'd have to run hoogle data to gather the information first. It requires wget/curl, so if you're on Windows you'd probably need to get this in your path first (or a curl for Windows, of course). On Linux it's almost always built-in (if you don't have it on Linux, just apt-get it). I never use Hoogle from the command-line by the way, it's simply not as accessible, but it can still be very helpful because some text-editors and their plugins can take advantage of it.
Alternatively you can use FPComplete's Hoogle which is sometimes more satisfying (because in my experience it has been aware of more 3rd party libraries. I only use it in those "Hoogling sessions").
There is also Hayoo! by the way.
1 In Hoogle you probably >95% of the time won't have to do this but +Module to import a module if for some reason it's not being searched for (which is the case sometimes for 3rd party libraries).
You can also filter away modules by -Module.
For instance: destroyTheWorld +World.Destroyer -World.Destroyer.Mercy to find destroyTheWorld and make sure you're not looking at the merciful way to do it (This comes very handy with modules with the same function names for different versions, like the ones in Data.ByteString & Data.ByteString.Lazy, Data.Vector & Data.Vector.Mutable, etc.).
Oh and one more awesome advantage of Hoogle is that not only it shows you the function's signature, it can also take you to the module's Haddock pages, so you also gain documentation + in those pages, when available, you can click on "Source" at the right of every function to see how it's being implemented for even more information.
This is outside the scope of the question, but Hoogle is also used to query for function signatures which is just.. mindblowingly helpful. If I want a function that takes an index number and a list and gives me the element in that index, and I wonder if it's already built in, I can search for it within seconds.
I know that the function takes a number and a list, and gives me an element of the list so the function signature must look something along these lines: Int -> [a] -> a (or generically: Num a => a -> [b] -> b), and both instances show up that there really is a function for that ((!!) and genericIndex).
Where GHCi has the upperhand is that you can toy with the expressions, explore them, etc. A lot of times when dealing with abstract functions that means a lot.
Being able to :l(oad) is very very helpful as-well.
If you're just looking for function signatures, you can combine both Hoogle and GHCi.
In GHCi you can type :! cmd, and GHCi will execute cmd in the command-line, and print the results. That means you can Hoogle inside GHCi too, e.g. :! hoogle void.
Start ghci, :cd to the base directory of the source you're reading, :load the module you're interested in, and use the :i command to get the info:
ghci> :i <$>
(<$>) :: Functor f => (a -> b) -> f a -> f b
-- Defined in `Data.Functor'
infixl 4 <$>
ghci> :i $
($) :: (a -> b) -> a -> b -- Defined in `GHC.Base'
infixr 0 $
ghci> :i .
(.) :: (b -> c) -> (a -> b) -> a -> c -- Defined in `GHC.Base'
infixr 9 .
That tells you the type, where it's defined, the associativity (infixl or infixr) and the precedence (the number; higher is tighter). So (*10) <$> a $ 1 is read as ((*10) <$> a) $ 1.
When you :load a module, all of the names that are in scope inside that module will be in scope inside ghci. The one place where this can get annoying is if you have an error in the code, then you can't :i anything inside it. In these cases, you can comment lines out, use undefined, and probably also use typed holes as behlkir suggests (haven't played with those too much yet).
While you're at it, try the :? command in ghci.
Related
So I just recently learned about and started using TypeApplications, and was wondering how we can generally know what type variables we're assigning. The documentation on TypeApplications I found mentions:
What order is used to instantiate the type variables?
Left-to-right order of the type variables appearing in the foralls. This is the most logical order that occurs when the instantiation is done at the type-variable level. Nested foralls work slightly differently, but at a single forall location with multiple variables, left-to-right order takes place. (See below for nested foralls).
However I haven't found mention of how order of type variables in implicit foralls is determined. I tried looking at different examples with -fprint-explicit-foralls to see if there was a simple pattern, but I'm getting different results in different versions of ghci. :/
In ghci version 8.0.2, I get:
> :t (,,)
(,,) :: forall {c} {b} {a}. a -> b -> c -> (a, b, c)
while in ghci version 8.4.3, I get:
> :t (,,)
(,,) :: forall {a} {b} {c}. a -> b -> c -> (a, b, c)
Then again, maybe that's simply a bug in how foralls were being printed in 8.0.2, because otherwise type applications seem to be done right-to-left with the variables of forall, contrary to what the documentation says:
> :t (,,) #Bool
(,,) #Bool :: forall {c} {b}. Bool -> b -> c -> (Bool, b, c)
So are type variables put in implicit foralls always in the order they appear first left-to-right in the type body (including the constraints)?
TL;DR: Type variable order is determined by first left-to-right encounter. When in doubt, use :type +v.
Don't use :type
Using :type is misleading here. :type infers the type of a whole expression. So when you write :t (,), the type checker looks at
(,) :: forall a b. a -> b -> (a, b)
and instantiates all the forall’s with fresh type variables
(,) :: a1 -> b1 -> (a1, b1)
which is necessary if you would apply (,). Alas, you don’t, so type inference is almost done, and it generalizes over all free variables, and you get, for example,
(,) :: forall {b} {a}. a -> b -> (a, b)
This step makes no guarantees over the order of the free variable, and the compiler is free to change.
Also note that it writes forall {a} instead of forall a, which means that you can’t use visible type application here.
Use :type +v
But of course you can use (,) #Bool – but here the type-checker treats the first expression differently and does not do this instantiation/generalization step.
And you can get this behaviour in GHCi as well – pass +v to :type:
:type +v (,)
(,) :: forall a b. a -> b -> (a, b)
:type +v (,) #Bool
(,) #Bool :: forall b. Bool -> b -> (Bool, b)
Look, no {…} around type variables!
Where does this order come from?
The the GHC user's guide section on visible type application states:
If an identifier’s type signature does not include an explicit forall, the type variable arguments appear in the left-to-right order in which the variables appear in the type. So, foo :: Monad m => a b -> m (a c) will have its type variables ordered as m, a, b, c.
That this only applies to things that have an explicit type signature. There is no guaranteed order to variables in inferred types, but you also can't use expressions with inferred types with VisibleTypeApplication.
When I ask about the foldl type, what I see is this:
*Main> :t foldl
foldl :: Foldable t => (b -> a -> b) -> b -> t a -> b
In this case, what is t a?
I guess it means that the function is using a Foldable parametrized with a but I'm not even really sure about the syntax. For instance, why can't I substitute t a with Foldable a?
And, bonus question, if I had to define foldl myself, I would start with the base case
foldl f b [] = []
But if the base case takes a list, than it would not make much sense to accept a Foldable. What is the "empty Foldable" that I could use as the base case?
Foldable is something called a "typeclass". Saying Foldable t => declares that t must implement the requirements of Foldable. However, t remains t, and doesn't get collapsed into a reference to a Foldable interface like it might in Java. That's why you can't just have Foldable a.
Definitely check out https://hackage.haskell.org/package/base-4.10.1.0/docs/Data-Foldable.html for an explanation of the requirements of Foldable and what methods that gets you.
Anyways, if you want to use one of Foldable's methods, then either use a known Foldable type like Set:
import qualified Data.Set as S
import Data.Foldable
sumSet :: (Num n) => S.Set n -> n
sumSet ns = foldl' (\ n acc -> n + acc ) 0 ns --make this pointfree if you want
Or you can take a type parameter and constrain it to be foldable:
sumFld :: (Num n, Foldable f) => f n -> n --different signature
sumFld ns = foldl' (\ n acc -> n + acc ) 0 ns --same implementation!
The following prints 6, twice:
main :: IO ()
main = print (sumSet $ S.fromList [1, 2, 3]) >> print (sumFld $ S.fromList [1, 2, 3])
I like the other answers, which discuss briefly the distinction between types and classes and so answer the first half of the question in that way. To keep this answer complete, I'll reiterate this very briefly; but see the other answers for longer explanations, since I want to spend the bulk of my time on the second half of the question.
So: Foldable is a typeclass (a collection of types). The t in the type of foldl rangers over types, not collections of types, and this explains why it can't be replaced by Foldable.
But I think this is also a very interesting question that the other answers didn't address:
If I had to define foldl myself, I would start with the base case
foldl f b [] = []
But if the base case takes a list, than it would not make much sense to accept a Foldable.
If you were to define foldl yourself, you would be creating an instance of Foldable for a specific type. Because in the instance you know which specific type is involved, you can write base cases for that type and need not know about any other instances of Foldable. In the Foldable instance for lists:
instance Foldable [] where
foldl f b [] = b
...is perfectly well-typed, because we know t ~ []. On the other hand, in the instance for another type, we would of course have to use a different base case. For example:
import qualified Data.Set as S
instance Foldable S.Set where
foldl f b s | S.null s = b
Again, this is okay inside the instance block because then we have t ~ S.Set. We can (and generally have to) write things that only work on S.Sets and would not work well with other Foldable instances.
foldl :: Foldable t => (b -> a -> b) -> b -> t a -> b
In this case, what is t a?
I guess it means that the function is using a Foldable parametrized with a [...]
That's exactly right. Note, however, that it is "a Foldable parameterized with a", and not "Foldable parametrized with a". As hegel5000's answer explains, when you write Foldable t to the left of =>, you are saying something about the type (constructor) t. Replacing t a with Foldable a would be a little like putting an adjective where a noun should be.
Moving on to your bonus question: foldl happens to be a method of the Foldable class. That means you are able to, when writing an instance of Foldable for a type, to supply an appropriate definition of foldl in which you can make use of the specific features of the type (e.g. that [] is a constructor of lists -- see also Daniel Wagner's answer).
If foldl weren't a method of Foldable, though, the only things you would be able to use to deal with the t a argument when implementing it would be the (other) methods of Foldable (foldr, foldMap, etc.), as you don't know anything else about t other than the fact that it has a Foldable instance. (Perhaps surprisingly, it is possible to implement foldl in such a way. Such an implementation is provided as a default for the foldl method in the Foldable class, so that you don't have to implement it yourself when writing an instance if you don't consider it necessary.)
In the context (Foldable t) => ... the t is the foldable type. An example of this could be the list type [].
Some more concrete types for foldl would be
foldl :: (b -> a -> b) -> b -> [a] -> b
foldl :: (b -> a -> b) -> b -> Tree a -> b
Foldable t => ... can be read as t is required to be a Foldable. It is not the type constructor Foldable applied to the type t! Therefore Foldable t is not even a type and you can't ever use it on the right hand side of a =>.
I was messing around with the runST function. Which has type (forall s. ST s a) -> a and it seems like trying to use it in any way that isn't directly applying without any indirection breaks it in pretty nasty ways.
runST :: (forall s. ST s a) -> a
const :: a -> b -> a
so by substituting a in const for forall s. ST s a you should get the type of const runST
const runST :: b -> (forall s. ST s a) -> a
but instead GHC says that it can't match a with (forall s. ST s a) -> a but since a literally means forall a. a which is satisfied by every type I don't see what is invalid about that.
As it turns out using \_ -> runST instead actually works just fine and gives the correct type.
Once I had constST with the correct type I wanted to see if I could uncurry it, but unsurprisingly that also breaks. But it seems like it really shouldn't, although admittedly this case seems less obvious than the previous one:
constST :: b -> (forall s. ST s a) -> a
uncurry :: (a -> b -> c) -> (a, b) -> c
So then surely the a in uncurry could be replaced with the b in constST and the b in uncurry could be replaced with the (forall s. ST s a) in constST and the a in uncurry could be replaced with the c in constST. This gives us:
uncurry constST :: (b, forall s. ST s a) -> a
Now admittedly this type is impredicative which I know is pretty problematic. But technically Just mempty is also impredicative when substituting directly without moving the implicit forall quantification.
Just :: a -> Maybe a
mempty :: forall a. Monoid a => a
Just mempty :: Maybe (forall a. Monoid a => a)
But the forall is automatically floated up to give you:
Just mempty :: forall a. Monoid a => Maybe a
Now if we do the same thing for uncurry constST we should get the sensible and as far as I can tell correct type of:
uncurry constST :: (forall s. (b, ST s a)) -> a
Which is higher rank but not impredicative.
Can someone explain to me why basically none of the above actually works with GHC 8, is there something more fundamental that makes the above very complicated in the general case? Because if not it seems like it would be really nice to have the above work, if only to get rid of the annoying special casing of $ purely for the sake of runST.
On a side note is it possible that instead of all the forall floating we could instead have ImpredicativeTypes just work correctly. It correctly infers the type for Just mempty as Maybe (forall a. Monoid a => a) but it seems like actually using it is not that easy. I have heard that impredicative type inference is not really doable but would it work to somehow limit type inference to predicative types except when you provide type signatures to indicate otherwise. Similar to how MonoLocalBinds makes local bindings monomorphic by default for the sake of type inference.
You have answered your own question:
... by substituting a in const for forall s. ST s a ...
This is the definition of impredictive polymorphism - the ability to instantiate a type variable with a polytype, which is (loosely) a type with a forall quantifier at the leftmost side of the type.
From the GHC trac page on the subject:
GHC does not (yet) support impredicative polymorphism
and furthermore
We've made various attempts to support impredicativity, so there is a flag -XImpredicativeTypes. But it doesn't work, and is absolutely unsupported. If you use it, you are on your own; I make no promises about what will happen.
So do not use ImpredictiveTypes - it won't help.
Now for the gory details - why do all the specific examples work as they do?
You've noted that in the expression Just mempty, the impredictive type Maybe (forall a. Monoid a => a) is not inferred; instead, the forall is 'floated out'. You also noted that performing the same process for uncurry constST gives a type "which is higher rank but not impredicative". The GHC user guide has this to say about higher rank types:
In general, type inference for arbitrary-rank types is undecidable. ...
For a lambda-bound or case-bound variable, x, either the programmer provides an explicit polymorphic type for x, or GHC’s type inference will assume that x’s type has no foralls in it.
So you really have to help it quite a bit, and that usually precludes using higher-order functions alltogether (note the above says nothing about arbitrary applications, only about bound variables - and uncurry constST has no bound variables!). The correct type for Just mempty is rank 1, so there is no issue inferring it, with or without additional type signatures.
For example, you can write your (forall s. (b, ST s a)) -> a function as such (on GHC 8.0.1 at least):
constST' :: forall a b . (forall s . (b, ST s a)) -> a
constST' z = runST z'
where z' :: forall s . ST s a
z' = snd z
and also note that you cannot even pattern match on the pair, because this immediately instantiates the bound b type var:
constST' :: forall a b . (forall s . (b, ST s a)) -> a
constST' (a,b) = _res
Using typed holes, you get:
* Found hole: _res :: a
Where: `a' is a rigid type variable bound by
the type signature for:
constST' :: forall a b. (forall s. (b, ST s a)) -> a
* In the expression: _res
In an equation for constST': constST' (a, b) = _res
* Relevant bindings include
b :: ST s0 a
a :: b
Note the type of b is ST s0 a for some fresh type variable s0, not the required forall s . ST s a for runST. There is no way to get the old type back.
The simplest solution to such things is probably to define a newtype, as the GHC trac page suggests:
newtype RunnableST a = RST (forall s . ST s a)
rrunST :: RunnableST a -> a
rrunST (RST a) = runST a
And store your ST actions which are ready to run in this container:
doSomething :: RunnableST X
doSomething = RST $ do ...
To be able to write const runST, you need to active the Impredicative types extension (on GHCi: :set -XImpredicativeTypes).
My question is very simple, as anyone beginning haskell I've been thinking about types, function composition and how to apply them. I started thinking about what the result of ((+) . (*)) might be.
Now obviously the solution to that question is open ghci and find out. So I did that and inspected the type:
λ> :t ((*) . (+))
((*) . (+)) :: (Num (a -> a), Num a) => a -> (a -> a) -> a -> a
Is this type possible? I'm struggling to understand what it might be or what it means?
apologies again for the simplistic question, everything I tried to put into the function failed. I'm simply trying to develop intuition for function composition with dyadic functions.
Unfortunately, GHC doesn't give a very good message for this. This is almost certainly not what you want. There is no instance for Num (a -> a) by default and implementing one, though it could be useful for some things, could lead to very unintuitive runtime errors. Even in this case, this function would not likely be very useful.
Let's look at type restricted (*) and (+) to simplify the situations and avoid the added complexity of the type class:
(*!) :: Int -> Int -> Int
(*!) = (*)
(+!) :: Int -> Int -> Int
(+!) = (+)
Now when we try
λ> :t (*!) . (+!)
<interactive>:1:8:
Couldn't match type ‘Int -> Int’ with ‘Int’
Expected type: Int -> Int
Actual type: Int -> Int -> Int
Probable cause: ‘(+!)’ is applied to too few arguments
In the second argument of ‘(.)’, namely ‘(+!)’
In the expression: (*!) . (+!)
which indicates that we aren't applying (+!) to enough arguments to be able to apply its result to (*!). If we expand the function composition we see this, which might make it more clear why it doesn't make sense:
(*!) . (+!) == \x -> (*!) ((+!) x) -- definition of (.)
== \x y -> (*!) ((+!) x) y -- eta-expansion
== \x y -> ((+!) x) *! y -- changed (*.) from prefix to infix
The left argument of (*!) is a function, which doesn't match the expected type of Int.
Composing with a function of two arguments
In order to do this, we need a function (b -> c) -> (a1 -> a2 -> b) -> a1 -> a2 -> c. Luckily, that's exactly what ((.) . (.)) is.
λ> :t ((.) . (.)) (*!) (+!)
((.) . (.)) (*!) (+!) :: Int -> Int -> Int -> Int
Some libraries provide this under the name (.:) (like this one). Sometimes people like to write it fmap . fmap (this generalizes to more than just normal function composition though).
It's a bit cryptic though and is usually avoided for that reason. It's almost always more clear to just explicitly write out the function.
that's a fun question....
First of all, I'll explain why you get the type you do.
Both (+) and (*) have type
Num a=>a->a->a
which basically means that they have two numbers as input, and output one number (as should be expected from add and multiply)
Function composition chains two functions of type (a->b) together (where, of course, the output of the first needs to be the same type as the input to the next).
(.)::(b->c)->(a->b)->a->c
So, at first glance, neither (+) or (*) seem to be of that type.... Except that in the world of Haskell, you can think of them as type
(+)::Num a=>a->(a->a)
which makes sense.... If you fill in one value of (+), you get a function that increments the value of a number by that amount, for instance
(+) 1 --evaluates to incrementByOne
where incrementByOne x = 1+x
So, you can chain the two together, but.....
Remember, the inputs of (*) need to be numbers! (because of the Num a=>)
Applying (.) to (+) and (*) yields your type
(Num (a -> a), Num a) => a -> (a -> a) -> a -> a
But, it has the strange constraint Num (a->a), which states that a function needs to be a number. This is basically not supposed to be done, but there is nothing in Haskell that would forbid it, so the compiler doesn't complain at this point. It is only when you try to use the function that it performs the check.
((+) . (*)) 1 (+ 1) 2
<interactive>:16:1:
No instance for (Num (a0 -> a0)) arising from a use of ‘it’
In a stmt of an interactive GHCi command: print it
The three inputs have the correct type, except for the constraint.... The interpreter is complaining here that the function (+ 1) is not a number.
I don't understand what "lifting" is. Should I first understand monads before understanding what a "lift" is? (I'm completely ignorant about monads, too :) Or can someone explain it to me with simple words?
Lifting is more of a design pattern than a mathematical concept (although I expect someone around here will now refute me by showing how lifts are a category or something).
Typically you have some data type with a parameter. Something like
data Foo a = Foo { ...stuff here ...}
Suppose you find that a lot of uses of Foo take numeric types (Int, Double etc) and you keep having to write code that unwraps these numbers, adds or multiplies them, and then wraps them back up. You can short-circuit this by writing the unwrap-and-wrap code once. This function is traditionally called a "lift" because it looks like this:
liftFoo2 :: (a -> b -> c) -> Foo a -> Foo b -> Foo c
In other words you have a function which takes a two-argument function (such as the (+) operator) and turns it into the equivalent function for Foos.
So now you can write
addFoo = liftFoo2 (+)
Edit: more information
You can of course have liftFoo3, liftFoo4 and so on. However this is often not necessary.
Start with the observation
liftFoo1 :: (a -> b) -> Foo a -> Foo b
But that is exactly the same as fmap. So rather than liftFoo1 you would write
instance Functor Foo where
fmap f foo = ...
If you really want complete regularity you can then say
liftFoo1 = fmap
If you can make Foo into a functor, perhaps you can make it an applicative functor. In fact, if you can write liftFoo2 then the applicative instance looks like this:
import Control.Applicative
instance Applicative Foo where
pure x = Foo $ ... -- Wrap 'x' inside a Foo.
(<*>) = liftFoo2 ($)
The (<*>) operator for Foo has the type
(<*>) :: Foo (a -> b) -> Foo a -> Foo b
It applies the wrapped function to the wrapped value. So if you can implement liftFoo2 then you can write this in terms of it. Or you can implement it directly and not bother with liftFoo2, because the Control.Applicative module includes
liftA2 :: Applicative f => (a -> b -> c) -> f a -> f b -> f c
and likewise there are liftA and liftA3. But you don't actually use them very often because there is another operator
(<$>) = fmap
This lets you write:
result = myFunction <$> arg1 <*> arg2 <*> arg3 <*> arg4
The term myFunction <$> arg1 returns a new function wrapped in Foo:
ghci> :type myFunction
a -> b -> c -> d
ghci> :type myFunction <$> Foo 3
Foo (b -> c -> d)
This in turn can be applied to the next argument using (<*>), and so on. So now instead of having a lift function for every arity, you just have a daisy chain of applicatives, like this:
ghci> :type myFunction <$> Foo 3 <*> Foo 4
Foo (c -> d)
ghci: :type myFunction <$> Foo 3 <*> Foo 4 <*> Foo 5
Foo d
Paul's and yairchu's are both good explanations.
I'd like to add that the function being lifted can have an arbitrary number of arguments and that they don't have to be of the same type. For example, you could also define a liftFoo1:
liftFoo1 :: (a -> b) -> Foo a -> Foo b
In general, the lifting of functions that take 1 argument is captured in the type class Functor, and the lifting operation is called fmap:
fmap :: Functor f => (a -> b) -> f a -> f b
Note the similarity with liftFoo1's type. In fact, if you have liftFoo1, you can make Foo an instance of Functor:
instance Functor Foo where
fmap = liftFoo1
Furthermore, the generalization of lifting to an arbitrary number of arguments is called applicative style. Don't bother diving into this until you grasp the lifting of functions with a fixed number of arguments. But when you do, Learn you a Haskell has a good chapter on this. The Typeclassopedia is another good document that describes Functor and Applicative (as well as other type classes; scroll down to the right chapter in that document).
Hope this helps!
Let's start with an example (some white space is added for clearer presentation):
> import Control.Applicative
> replicate 3 'a'
"aaa"
> :t replicate
replicate :: Int -> b -> [b]
> :t liftA2
liftA2 :: (Applicative f) => (a -> b -> c) -> (f a -> f b -> f c)
> :t liftA2 replicate
liftA2 replicate :: (Applicative f) => f Int -> f b -> f [b]
> (liftA2 replicate) [1,2,3] ['a','b','c']
["a","b","c","aa","bb","cc","aaa","bbb","ccc"]
> ['a','b','c']
"abc"
liftA2 transforms a function of plain types to a function of same types wrapped in an Applicative, such as lists, IO, etc.
Another common lift is lift from Control.Monad.Trans. It transforms a monadic action of one monad to an action of a transformed monad.
In general, "lift" lifts a function/action into a "wrapped" type (so the original function gets to work "under the wraps").
The best way to understand this, and monads etc., and to understand why they are useful, is probably to code and use it. If there's anything you coded previously that you suspect can benefit from this (i.e. this will make that code shorter, etc.), just try it out and you'll easily grasp the concept.
I wanted to write an answer because I wanted to write it from a different point of view.
Let's say you have a functor like for example Just 4 and you wanted to apply a function on that functor for example (*2). So you try something like this:
main = print $ (*2) (Just 4)
And you'll get an error:
No instance for (Num (Maybe a0)) arising from an operator section
• In the expression: * 2
Ok that fails. To make (*2) work with Just 4 you can lift it with the help of fmap.
The type signature of fmap:
fmap :: (a -> b) -> f a -> f b
Link:
https://hackage.haskell.org/package/base-4.16.0.0/docs/Prelude.html#v:fmap
The fmap function takes an a -> b function, and a functor. It applies the a -> b function to the functor value to produce f b.
In other words, the a -> b function is being made compatible with the functor. The act of transforming a function to make it compatible is lifting.
In o.o.p. terms this is called an adapter.
So fmap is a lifting function.
main = print $ fmap (*2) (Just 4)
This produces:
Just 8
According to this shiny tutorial, a functor is some container (like Maybe<a>, List<a> or Tree<a> that can store elements of some another type, a). I have used Java generics notation, <a>, for element type a and think of the elements as berries on the tree Tree<a>. There is a function fmap, which takes an element conversion function, a->b and container functor<a>. It applies a->b to every element of the container effectively converting it into functor<b>. When only first argument is supplied, a->b, fmap waits for the functor<a>. That is, supplying a->b alone turns this element-level function into the function functor<a> -> functor<b> that operates over containers. This is called lifting of the function. Because the container is also called a functor, the Functors rather than Monads are a prerequisite for the lifting. Monads are sort of "parallel" to lifting. Both rely on the Functor notion and do f<a> -> f<b>. The difference is that lifting uses a->b for the conversion whereas Monad requires the user to define a -> f<b>.