Search code examples
curryingpurescriptfunction-composition

How do composition and currying interact?


While doing exercises of chapter 3 of the Purescript by Example book was puzzled by this:

The exercise is to write an isInBook :: String -> String -> AddressBook -> Boolean

findEntryByName :: String -> String -> AddressBook -> Maybe Entry
findEntryByName first last = head <<< filter
  \entry -> entry.firstName == first && entry.lastName == last

isSomething :: forall a. Maybe a -> Boolean
isSomething a = case a of
  Nothing -> false
  Just _ -> true

AddressBook :: List Entry, and this does what I want it to do. (on a sidenote, is there a common name for isSomething? It seems like a common function to want; a proposition whether something isn't Nothing)

But this doesn't compile:

isInBook = isSomething <<< findEntryByName

And after some searching I found that this does:

isInBook fist last = isSomething <<< findEntryByName first last

Why? I found the compile error not terribly helpful, but that may be my inexperience:

 Could not match type
                   
    Function String
                   
  with type
         
    Maybe
         

while trying to match type String                       
                           -> List                      
                                { address :: ...        
                                , firstName :: String   
                                , lastName :: String    
                                }                       
                              -> Maybe                  
                                   { address :: ...     
                                   , firstName :: String
                                   , lastName :: String 
                                   }                    
  with type Maybe t1
while checking that expression findEntryByName
  has type t0 -> Maybe t1
in value declaration isInBook

where t0 is an unknown type
      t1 is an unknown type

Solution

  • I agree that the error message that you got wasn't particularly helpful. I'll try to break down why it happens.

    Basically it comes down to the type of <<<, which in the context of functions is:

    (<<<) :: (b -> c) -> (a -> b) -> a -> c
    

    So when you do isInBook = isSomething <<< findEntryByName, since isSomething has type Maybe a -> Boolean, we must have that b in the above "generic" type signature is Maybe a and c is Boolean. But the type of findEntryByName is String -> String -> AddressBook -> Maybe Entry, which would mean that a has to be String and b has to be the function type String -> AddressBook -> Maybe Entry. [Recall here that functions in Purescript are curried, so a -> b -> c means precisely a -> (b -> c).] We have inferred b to be 2 completely different things, and that is exactly what your error message is complaining about.

    Basically <<< only really works well with functions of one argument. This is what your corrected version does: findEntryByName first last is a function of type AddressBook -> Maybe Entry, which composes perfectly with isSomething :: Maybe a -> Boolean.

    You can use it with functions of more than one argument - after all, due to currying all functions in Purescript strictly speaking taking one argument - but as you saw with findEntryByName, trying to compose something with a function of more than one argument means that your next function itself has to take a function as argument.

    If the repetition of first and last in the correct expression, isInBook first last = isSomething <<< findEntryByName first last, bothers you, then you can if you want eta-reduce to get rid of last:

    isInBook first = \last -> isSomething <<< findEntryByName first last
                   = (isSomething <<< _) <<< (findEntryByName first)
    

    and then do similar with first to make it fully point-free. But the final expression will be fairly unreadable, and even the above looks much worse to me than the original. Fewer explicit arguments isn't always better.

    Oh, and your isSomething is indeed in the standard libraries, as isJust. It's not actually as commonly useful as you seem to think, because most of the time when you have a Just value you want to actually extract the underlying value and do something with it, rather than throw information away with a Boolean True. But well sometimes that is all you want, as in your case, and for those cases the function exists.