typeclasses / haskell-phrasebook

The Haskell Phrasebook: a quick intro to Haskell via small annotated example programs
https://typeclasses.com/phrasebook
209 stars 22 forks source link

Records (with optics?) #9

Open chris-martin opened 4 years ago

chris-martin commented 4 years ago

Here's a records demo we wrote a while back:

import Numeric.Natural

data Person =
  Person
    { name :: String  -- ^ The person's given name
    , age :: Natural  -- ^ How many years old
    }
    deriving Show

main =
  do
    let a = Person { name = "Alice", age = 47 }
    let b = Person { name = "Bob", age = 50 }
    let c = b{ age = 51 }

    putStrLn (show a)
    putStrLn (show b)
    putStrLn (show c)

    putStrLn ("name: " ++ name c)
    putStrLn ("age: " ++ show (age c))
$ runhaskell records.hs
Person {name = "Alice", age = 47}
Person {name = "Bob", age = 50}
Person {name = "Bob", age = 51}
name: Bob
age: 51

Lately I'm feeling like the Phrasebook should just immediately introduce optics from the start.

{-# LANGUAGE TemplateHaskell #-}

import Numeric.Natural
import Optics

data Person =
  Person
    { _name :: String  -- ^ The person's given name
    , _age :: Natural  -- ^ How many years old
    }
    deriving Show

makeLenses ''Person

main =
  do
    let a = Person { _name = "Alice", _age = 47 }
    let b = Person { _name = "Bob", _age = 50 }
    let c = set age 51 b

    putStrLn (show a)
    putStrLn (show b)
    putStrLn (show c)

    putStrLn ("name: " ++ view name c)
    putStrLn ("age: " ++ show (view age c))

Maybe that's too radical. On the other hand, maybe introducing it now in an extremely simple context is good setup for a later page on doing "deep updates" with composed lenses. I think in a later page we could end up showing how to get a lot of of optics using only view, set, over, and (%) without being overwhelming.

cideM commented 4 years ago

Since I really like the idea of introducing optics as if it was something super simply and approachable and wanted to check out that new package anyway I'll get on it :100:

friedbrice commented 4 years ago

Lately I'm feeling like the Phrasebook should just immediately introduce optics from the start.

If that is a road you want to go down, I'd recommend taking a hard look at going the path of generic-lens (https://hackage.haskell.org/package/generic-lens) and generic-lens-labels (http://hackage.haskell.org/package/generic-lens-labels) rather than that of makeLenses. With the two above-mentioned packages, you get to avoid any magic strings in the names of your record fields. Access and manipulation looks like view #name person and over #name (map toUpper) person, respectively. Slightly annoyingly, you need to import the two libraries and set XOverloadedLabels in each module where you want to use this syntax, though, which maybe outweighs any potential gain.

I'm a big fan of generic-lens, but I can see why you might not want to use it. In your case, it's not an engineering trade-off and a pedagogical trade-off.

friedbrice commented 4 years ago

Either way you want to get your lenses, records with optics up front is--I think--the right way to go. Using optics to modify a deeply-nested field is a way in which Haskell records are genuinely, objectively better than imperative languages. E.g. in imperative code, you can use += to apply a (terribly specific) function to a deeply-nested field (of a very specific type), whereas over lets you do that for any function at any type! I think you should showcase that :-)