Skip to content

Commit 580ba98

Browse files
committed
Describe property accessors and add corresponding exercise.
1 parent 7326fab commit 580ba98

File tree

2 files changed

+51
-3
lines changed

2 files changed

+51
-3
lines changed

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

Lines changed: 4 additions & 0 deletions
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 >>> ((==) streetName)) >>> head
18+
1519
isInBook :: String -> String -> AddressBook -> Boolean
1620
isInBook firstName lastName book = not null $ filter filterEntry book
1721
where

text/chapter3.md

Lines changed: 47 additions & 3 deletions
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 an 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 provides an equivalent shorthand, where an underscore is followed by a field name, 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.
@@ -563,7 +595,7 @@ Likewise, in the code for `findEntry` above, we used a different form of functio
563595

564596
This is equivalent to the usual application `head (filter filterEntry book)`
565597

566-
`($)` is just an alias for a regular function called `apply`, which is defined in the Prelude. It is defined as follows:
598+
`$` is just an alias for a regular function called `apply`, which is defined in the Prelude. It is defined as follows:
567599

568600
```haskell
569601
apply :: forall a b. (a -> b) -> a -> b
@@ -579,13 +611,24 @@ But why would we want to use `$` instead of regular function application? The re
579611
For example, the following nested function application, which finds the street in the address of an employee's boss:
580612

581613
```haskell
582-
street (address (boss employee))
614+
_.street (_.address (_.boss employee))
583615
```
584616

585617
becomes (arguably) easier to read when expressed using `$`:
586618

587619
```haskell
588-
street $ address $ boss employee
620+
_.street $ _.address $ _.boss employee
621+
```
622+
623+
Note that neither of the above examples is idiomatic PureScript. Real-world code is more likely to express this as:
624+
```haskell
625+
(boss employee).address.street
626+
```
627+
or
628+
```haskell
629+
_.boss.address.street employee
630+
```
631+
589632
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:
590633
```text
591634
> mod 8 3
@@ -653,6 +696,7 @@ I will let you make your own decision which definition is easier to understand,
653696

654697
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.
655698
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`.
699+
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.
656700
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.
657701
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.
658702

0 commit comments

Comments
 (0)