Skip to content

Commit c11f881

Browse files
Describe prefix/infix functions and record field accessors in Chapter 3 (#285)
* Add description of prefix functions versus infix operators. * Describe property accessors and add corresponding exercise. * Update text/chapter3.md Co-authored-by: milesfrain <[email protected]> * Update exercises/chapter3/test/no-peeking/Solutions.purs Co-authored-by: milesfrain <[email protected]> * Update text/chapter3.md Co-authored-by: milesfrain <[email protected]> * Update text/chapter3.md Co-authored-by: milesfrain <[email protected]> * Update text/chapter3.md Co-authored-by: milesfrain <[email protected]> * Describe infix vs prefix based on AddressBook example. * Additional edits to #285 Wanted to make this a PR to a PR branch for easier commenting on proposed changes. #285 * Update text/chapter3.md Co-authored-by: milesfrain <[email protected]> Co-authored-by: Miles Frain <[email protected]>
1 parent 65c562c commit c11f881

File tree

2 files changed

+136
-11
lines changed

2 files changed

+136
-11
lines changed

exercises/chapter3/test/no-peeking/Solutions.purs

+4
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ findEntryByStreet streetName = filter filterEntry >>> head
1212
filterEntry :: Entry -> Boolean
1313
filterEntry e = e.address.street == streetName
1414

15+
-- Example alternative implementation using property accessor and composition
16+
findEntryByStreet' :: String -> AddressBook -> Maybe Entry
17+
findEntryByStreet' streetName = filter (_.address.street >>> eq streetName) >>> head
18+
1519
isInBook :: String -> String -> AddressBook -> Boolean
1620
isInBook firstName lastName book = not null $ filter filterEntry book
1721
where

text/chapter3.md

+132-11
Original file line numberDiff line numberDiff line change
@@ -479,6 +479,38 @@ This process is called _eta conversion_, and can be used (along with some other
479479

480480
In the case of `insertEntry`, _eta conversion_ has resulted in a very clear definition of our function - "`insertEntry` is just cons on lists". However, it is arguable whether point-free form is better in general.
481481

482+
## Property Accessors
483+
484+
One common pattern is to use a function to access individual fields (or "properties") of a record. An inline function to extract an `Address` from an `Entry` could be written as:
485+
486+
```haskell
487+
\entry -> entry.address
488+
```
489+
490+
PureScript also allows [_property accessor_](https://github.com/purescript/documentation/blob/master/language/Syntax.md#property-accessors) shorthand, where an underscore acts as the anonymous fuction argument, so the inline function above is equivalent to:
491+
492+
```haskell
493+
_.address
494+
```
495+
496+
This works with any number of levels or properties, so a function to extract the city associated with an `Entry` could be written as:
497+
498+
```haskell
499+
_.address.city
500+
```
501+
502+
For example:
503+
504+
```text
505+
> address = { street: "123 Fake St.", city: "Faketown", state: "CA" }
506+
> entry = { firstName: "John", lastName: "Smith", address: address }
507+
> _.lastName entry
508+
"Smith"
509+
510+
> _.address.city entry
511+
"Faketown"
512+
```
513+
482514
## Querying the Address Book
483515

484516
The last function we need to implement for our minimal address book application will look up a person by name and return the correct `Entry`. This will be a nice application of building programs by composing small functions - a key idea from functional programming.
@@ -529,7 +561,7 @@ This type signature says that `findEntry` takes two strings, the first and last
529561
And here is the definition of `findEntry`:
530562

531563
```haskell
532-
findEntry firstName lastName book = head $ filter filterEntry book
564+
findEntry firstName lastName book = head (filter filterEntry book)
533565
where
534566
filterEntry :: Entry -> Boolean
535567
filterEntry entry = entry.firstName == firstName && entry.lastName == lastName
@@ -547,11 +579,76 @@ Note that, just like for top-level declarations, it was not necessary to specify
547579

548580
## Infix Function Application
549581

550-
In the code for `findEntry` above, we used a different form of function application: the `head` function was applied to the expression `filter filterEntry book` by using the infix `$` symbol.
582+
Most of the functions discussed so far used _prefix_ function application, where the function name was put _before_ the arguments. For example, when using the `insertEntry` function to add an `Entry` (`john`) to an empty `AddressBook`, we might write:
583+
584+
```haskell
585+
> book1 = insertEntry john emptyBook
586+
```
587+
588+
However, this chapter has also included examples of _infix_ [binary operators](https://github.com/purescript/documentation/blob/master/language/Syntax.md#binary-operators), such as the `==` operator in the definition of `filterEntry`, where the operator is put _between_ the two arguments. These infix operators are actually defined in the PureScript source as infix aliases for their underlying _prefix_ implementations. For example, `==` is defined as an infix alias for the prefix `eq` function with the line:
589+
590+
```haskell
591+
infix 4 eq as ==
592+
```
593+
594+
and therefore `entry.firstName == firstName` in `filterEntry` could be replaced with the `eq entry.firstName firstName`. We'll cover a few more examples of defining infix operators later in this section.
595+
596+
There are situations where putting a prefix function in an infix position as an operator leads to more readable code. One example is the `mod` function:
551597

552-
This is equivalent to the usual application `head (filter filterEntry book)`
598+
```text
599+
> mod 8 3
600+
2
601+
```
602+
603+
This is fine, but doesn't line up with common usage (in conversation, one might say "eight mod three"). Wrapping a prefix function in backticks (\`) lets you use that it in infix position as an operator, e.g.,
604+
605+
```text
606+
> 8 `mod` 3
607+
2
608+
```
553609

554-
`($)` is just an alias for a regular function called `apply`, which is defined in the Prelude. It is defined as follows:
610+
In the same way, wrapping `insertEntry` in backticks turns it into an infix operator, such that `book1` and `book2` below are equivalent:
611+
612+
```haskell
613+
book1 = insertEntry john emptyBook
614+
book2 = john `insertEntry` emptyBook
615+
```
616+
617+
We can make an `AddressBook` with multiple entries by using multiple applications of `insertEntry` as a prefix function (`book3`) or as an infix operator (`book4`) as shown below:
618+
619+
```haskell
620+
book3 = insertEntry john (insertEntry peggy (insertEntry ned emptyBook))
621+
book4 = john `insertEntry` (peggy `insertEntry` (ned `insertEntry` emptyBook))
622+
```
623+
624+
We can also define an infix operator alias (or synonym) for `insertEntry.` We'll arbitrarily choose `++` for this operator, give it a [precedence](https://github.com/purescript/documentation/blob/master/language/Syntax.md#precedence) of `5`, and make it right [associative](https://github.com/purescript/documentation/blob/master/language/Syntax.md#associativity) using `infixr`:
625+
626+
```haskell
627+
infixr 5 insertEntry as ++
628+
```
629+
630+
This new operator lets us rewrite the above `book4` example as:
631+
632+
```haskell
633+
book5 = john ++ (peggy ++ (ned ++ emptyBook))
634+
```
635+
636+
and the right associativity of our new `++` operator lets us get rid of the parentheses without changing the meaning:
637+
638+
```haskell
639+
book6 = john ++ peggy ++ ned ++ emptyBook
640+
```
641+
642+
Another common technique for eliminating parens is to use `apply`'s infix operator `$`, along with your standard prefix functions.
643+
644+
For example, the earlier `book3` example could be rewritten as:
645+
```haskell
646+
book7 = insertEntry john $ insertEntry peggy $ insertEntry ned emptyBook
647+
```
648+
649+
Substituting `$` for parens is usually easier to type and (arguably) easier to read. A mnemonic to remember the meaning of this symbol is to think of the dollar sign as being drawn from two parens that are also being crossed-out, suggesting the parens are now unnecessary.
650+
651+
Note that `$` isn't special syntax that's hardcoded into the language. It's simply the infix operator for a regular function called `apply`, which is defined in the Prelude as follows:
555652

556653
```haskell
557654
apply :: forall a b. (a -> b) -> a -> b
@@ -560,20 +657,43 @@ apply f x = f x
560657
infixr 0 apply as $
561658
```
562659

563-
So `apply` takes a function and a value and applies the function to the value. The `infixr` keyword is used to define `($)` as an alias for `apply`.
660+
The `apply` function takes another function (of type `(a -> b)`) as its first argument and a value (of type `a`) as its second argument, then calls that function with that value. If it seems like this function doesn't contribute anything meaningful, you are absolutely correct! Your program is logically identical without it (see [referential transparency](https://en.wikipedia.org/wiki/Referential_transparency)). The syntactic utility of this function comes from the special properties assigned to its infix operator. `$` is a right-associative (`infixr`), low precedence (`0`) operator, which lets us remove sets of parentheses for deeply-nested applications.
564661

565-
But why would we want to use `$` instead of regular function application? The reason is that `$` is a right-associative, low precedence operator. This means that `$` allows us to remove sets of parentheses for deeply-nested applications.
662+
Another parens-busting opportunity for the `$` operator is in our earlier `findEntry` function:
663+
```haskell
664+
findEntry firstName lastName book = head $ filter filterEntry book
665+
```
666+
We'll see an even more elegant way to rewrite this line with "function composition" in the next section.
566667

567-
For example, the following nested function application, which finds the street in the address of an employee's boss:
668+
If you'd like to use a concise infix operator alias as a prefix function, you can surround it in parentheses:
568669

569-
```haskell
570-
street (address (boss employee))
670+
```text
671+
> 8 + 3
672+
11
673+
674+
> (+) 8 3
675+
11
676+
```
677+
678+
Alternatively, operators can be partially applied by surrounding the expression with parentheses and using `_` as an operand in an [operator section](https://github.com/purescript/documentation/blob/master/language/Syntax.md#operator-sections). You can think of this as a more convenient way to create simple anonymous functions (although in the below example, we're then binding that anonymous function to a name, so it's not so anonymous anymore):
679+
680+
```text
681+
> add3 = (3 + _)
682+
> add3 2
683+
5
571684
```
572685

573-
becomes (arguably) easier to read when expressed using `$`:
686+
To summarize, the following are equivalent definitions of a function that adds `5` to its argument:
574687

575688
```haskell
576-
street $ address $ boss employee
689+
add5 x = 5 + x
690+
add5 x = add 5 x
691+
add5 x = (+) 5 x
692+
add5 x = 5 `add` x
693+
add5 = add 5
694+
add5 = \x -> 5 + x
695+
add5 = (5 + _)
696+
add5 x = 5 `(+)` x -- Yo Dawg, I herd you like infix, so we put infix in your infix!
577697
```
578698

579699
## Function Composition
@@ -612,6 +732,7 @@ I will let you make your own decision which definition is easier to understand,
612732

613733
1. (Easy) Test your understanding of the `findEntry` function by writing down the types of each of its major subexpressions. For example, the type of the `head` function as used is specialized to `AddressBook -> Maybe Entry`. _Note_: There is no test for this exercise.
614734
1. (Medium) Write a function `findEntryByStreet :: String -> AddressBook -> Maybe Entry` which looks up an `Entry` given a street address. _Hint_ reusing the existing code in `findEntry`. Test your function in PSCi and by running `spago test`.
735+
1. (Medium) Rewrite `findEntryByStreet` to replace `filterEntry` with the composition (using `<<<` or `>>>`) of: a property accessor (using the `_.` notation); and a function that tests whether its given string argument is equal to the given street address.
615736
1. (Medium) Write a function `isInBook` which tests whether a name appears in a `AddressBook`, returning a Boolean value. _Hint_: Use PSCi to find the type of the `Data.List.null` function, which tests whether a list is empty or not.
616737
1. (Difficult) Write a function `removeDuplicates` which removes "duplicate" address book entries. We'll consider entries duplicated if they share the same first and last names, while ignoring `address` fields. _Hint_: Use PSCi to find the type of the `Data.List.nubBy` function, which removes duplicate elements from a list based on an equality predicate. Note that the first element in each set of duplicates (closest to list head) is the one that is kept.
617738

0 commit comments

Comments
 (0)