On exercise is to check whether an addressbook entry is in an addressbook when two string string (firstname and last anme) are given. (Excercise 4). My solution is:
isInBook :: String -> String -> AddressBook -> Boolean
isInBook first last book = not null (filter filterEntry book)
where
filterEntry entry = entry.firstName == first && entry.lastName == last
This works fine. However I know if I have only one condition I can streamline this to a anonymous filter function and then write the none-explicit (eta?) function definition:
isInBook1 :: String -> String -> AddressBook -> Boolean
isInBook1 first last = not null <<< filter (eq first <<< _.firstName) -- omitting test for last!
Now my question is can I do an “and-compose” on the functions (eq first <<< _.firstName) and `(eq last <<< _.lastName) and then use this new (anonymous) function as argument to filter?
The problem is that you’re not just passing one single composed predicate to filter there.
To my understanding…
filter (eq first <<< _.firstName) && (eq last <<< _.lastName)
is parsed as
(filter (eq first <<< _.firstName))
&&
(eq last <<< _.lastName)
If seeing it like that makes sense.
That is, filter is only getting the first part of the predicate. And then you’re trying to &&-compose that entire thing with the second part of the predicate.
i.e. a bit like:
-- You're doing this
(filter pred1) && pred2
-- but what you want is
filter (pred1 && pred2)
You’ll find that the following works:
isInBookzz :: String -> String -> AddressBook -> Boolean
isInBookzz first last =
not A.null
<<< A.filter (eq first <<< _.first_name && eq last <<< _.last_name)
-- or
isInBookzzz :: String -> String -> AddressBook -> Boolean
isInBookzzz first last =
A.filter (eq first <<< _.first_name && eq last <<< _.last_name)
>>> not A.null
Note in the above that there is only one argument passed to A.filter (i.e. everything in the parentheses), but that no internal parentheses were needed for composing the predicate (i.e. && had the correct precedence there).
Here’s a different implementation you might consider:
hasBothNames :: String -> String -> SingleAddressEntry -> Boolean
hasBothNames first last =
eq first <<< _.first_name && eq last <<< _.last_name
findEntryByNames :: String -> String -> AddressBook -> Maybe SingleAddressEntry
findEntryByNames first last = A.find (hasBothNames first last)
isInBook :: String -> String -> AddressBook -> Boolean
isInBook first last =
maybe false (\_ -> true) <<< findEntryByNames first last
For the curious, note that && isn’t special/magical. Rather, it is simply an infix operator for conj and performs exactly the predicate composition that you need, allowing point-free style.
It actually has nothing to do with left or right associativity in this case, but rather precedence. Function application has a higher precedence than any operator so f foo * bar will always be (f foo) * bar.