Does Haskell provide a way to map a function to a data member?
Asked Answered
E

2

12

I am a Haskell rookie and I often find myself having to decompose a data with pattern matching only to apply a function to one of its member and then reassemble it.

Say I have:

data Car = Car { gas :: Int, licensePlate :: String }

and I want it to halve its gas when it drives, and refuel it, I'm doing:

mapGas:: (Int -> Int) -> Car -> Car
mapGas f (Car aGas aLicensePlate) = Car (f aGas) aLicensePlate

drive:: Car -> Car
drive = mapGas (flip div 2)

refuel:: Int -> Car -> Car
refuel = mapGas . (+)

Is there a way to do just that without having to define the auxiliary function mapGas? Since it can become rather bothersome having to write a map function for every member of the data when it's made of many fields. I know it is possible to assign a value to one of the members with accessors:

runOutOfFuel:: Car -> Car
runOutOfFuel aCar = aCar { gas = 0 }

Is it possible to map a function with accessors too? if so, how?

Etem answered 5/4, 2017 at 20:42 Comment(3)
I think you should look into some of the lens tutorials hackage.haskell.org/package/lensOutsell
Just for the record, without lenses, I think the best you can do is mapGas f = (\car -> car {gas = f (gas car)}). This at least avoids mentioning the other fields.Naphthyl
@Naphthyl What I usually do in those cases is try to preserve the symmetry: mapGas f = (\car@Car{ gas = g } -> car {gas = f g }). Longer, but you can see the gas field on both sides of ->.Atreus
A
13

Using just the core libraries? No. But with the widely used lens package, yes. Here is what that looks like in your case:

{-# LANGUAGE TemplateHaskell #-}

import Control.Lens.TH
import Control.Lens

data Car = Car { _gas :: Int, _licensePlate :: String }

makeLenses ''Car

Now, you can easily get/set/modify fields that are nested in data structures.

runOutOfFuel:: Car -> Car
runOutOfFuel = gas .~ 0

drive:: Car -> Car
drive = gas %~ (`div` 2)

refuel:: Int -> Car -> Car
refuel c = gas +~ c

The magic here is that makeLenses ''Car generates gas and licensePlate functions that are similar (but more powerful) to your mapGas (in fact, mapGas = (gas %~)). Getting started with lens is pretty daunting, but I recommend just reading the examples section.

Atreus answered 5/4, 2017 at 20:56 Comment(3)
Data.Data and GHC.Generics are in basic and can be used for this, but I agree that lens is usually the natural choiceMckale
I would also recommend this lens tutorial (if you want to understand lens better): artyom.me/lens-over-tea-1Grilse
I totally agree with the "pretty daunting". Sometimes I think it was a bit overgeneralized beyond practical common cases.Naphthyl
L
4

There is no language feature which does this, but, as with many things in Haskell, the core language is powerful enough that this can be implemented in a simple and elegant way.

The solution for what you are looking for is a kind of value called a lens. A lens does exactly what you want: It allows you to take any kind of abstract data and apply a function on a part of it, getting the entire data value with the modified part included as a result.

There's an introduction to lenses I quite like here. To use the examples included, you'll need the lens package. (Or this one if you're using Stack)

Laurustinus answered 5/4, 2017 at 20:53 Comment(4)
I don't know if I'd say that lens's existence is evidence that the core language is powerful enough. lens creation is explicitly done outside of the core language, using template haskell to generate a bunch of code because the core language is not powerful enough to let that code be written once in a general way and then reused.Touraco
You can actually create lenses without using templates, it just tends to be a bit tedious to do so people use templates to streamline it. As for being powerful, I meant that in the sense that a lot of stuff that needs to be hard-coded into other programming languages can be defined in Haskell.Laurustinus
@Touraco lens creation is only done with templating to reduce boilerplate. You could write all the lens functions by hand, it would just suck.Gerdi
@PedroCastilho @Gerdi You are missing @amalloy's point. He is aware of how lens works, he is just saying that the core language isn't powerful enough to do the same work that TH does. One could imagine having lenses automagically work (the same way getters are generated when you define a record) - but they don't. Hopefully this will change once OverloadedLabels supports setters.Atreus

© 2022 - 2024 — McMap. All rights reserved.