Trouble writing readable code using Haskell records

Viewed 178

I'm fairly new to Haskell, and one thing I've been struggling with is writing readable code using records.

My specific problems are:

  1. I haven't found an effective strategy for dealing with name conflicts between fields in different record types. I'm finding I want the same field in multiple different record types, and the name conflict issue is really annoying. I end up choosing some prefix to put on all of my fields, which adds to verbosity and hinders readability.
  2. Using nested records results in really verbose code. I find
    someFunction(foo.bar, 2 * foo.bar.baz)
    in a language like Java or C++ to be pretty readable. In Haskell I find myself writing this to accomplish the same thing
    someFunction (fooBar foo) (2 * barBaz (fooBar foo))
    which is a lot harder to visually parse, and calls to functions with multiple arguments quickly become unreadable. In order to make this more readable, I find myself defining intermediate values which are to extract fields from records, which is more readable, but adds more lines of code, so it hurts readability in a different way.

Is there a better way to use records that is more readable, or is there something I should be doing instead? Just using tuples? Writing functions with tons of parameters instead of grouping related values into records? Something else?

1 Answers

One solution (as suggested in the comments) to the problem is to use lenses. Using the microlens and microlens-th packages (these might be simpler when you're getting started):

{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE FunctionalDependencies #-}
{-# LANGUAGE FlexibleInstances #-}

import Data.List (nub)
import Lens.Micro ((^.), (^..))
import Lens.Micro.TH (makeFields)

newtype Name = Name String
  deriving Eq

data Person = Person { _personName :: Name }
makeFields ''Person

data Species = Dog | Cat
  deriving Eq

data Pet = Pet { _petName :: Name, _petSpecies :: Species }
makeFields ''Pet

-- ^. is an infix operator for view
uniquePersonNames :: [Person] -> [Name]
uniquePersonNames ps = nub (map (\p -> p ^. name) ps)

dogs :: [Pet] -> [Pet]
dogs ps = filter (\p -> p ^. species == Dog) ps

data Concert = Concert
  { _concertPerformers :: [Person]
  , _concertAttendees :: [Person]
  }
makeFields ''Concert

-- ^.. is an infix operator for toListOf
performerNames :: Concert -> [Name]
performerNames c = c ^.. performers . traverse . name

data House = House { _housePeople :: [Person], _housePet :: Pet}
makeFields ''House

houseSound :: House -> String
houseSound h = case h ^. pet . species of
    Dog -> "Woof!"
    Cat -> "Meow!"

There are several resources out there to learn more about lenses and other kinds of optics. One particularly beginner friendly resource Control.Lens.Tutorial.

Be warned that this approach can lead to type errors that are hard to understand (I believe the generic-lens library has better error messages, but I have not used it), especially if you start using things blindly. I suggest sticking to the basics (as presented in the linked tutorial) -- this will cover a large portion of your use cases.

Related