Skip to content

jinilover/apply-lens

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

12 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

apply-lens

There are numerous articles explaining what are lenses and why they are used. This repo shares my experience of using lens in the following scenarios:

  • Using simpler way to update an existing Map entry
  • Using Traversal to update a field or nested field of a data structure list

Update an existing Map entry

A common way to update an existing entry

-- Suppose, m :: M.Map k v, f :: v -> v
maybe m (M.insert k f) $ M.lookup k m

Using lens to simpify the code

import Control.Lens
ix k %~ f $ m
-- "%~" is "over" infix operator

Update a field of an item list

Consider the following types:

data CategoryFund = CategoryFund {
    _fCategory :: !String
  , _defaultAmount :: !Int
  , _maxAmount :: !Int
}

data District = District {
    _dName :: !String
  , _availableFund :: !Int
  , _categoryFunds :: ![CategoryFund]
}

data Contribution = Contribution {
    _district :: !String
  , _contribute :: !Int
}

data FinalBill = FinalBill {
    _fbName :: !String
  , _requiredFund :: !Int
  , _approvedFund :: !Int
  , _fullyFunded :: !Bool
  , _contributeFrom :: ![Contribution]
}

Suppose:

  • In District, if summation of _defaultAmount of [CategoryFund] is greater than _availableFund, all _defaultAmount values should be capped by _availableFund in proportion to the summation of _defaultAmount.
  • A similar requirement on _maxAmount of [CategoryFund].
  • A similar requirement on FinalBill if summation of _contribute of [Contribution] is greater than _requiredFund.

Challenge:

  • The logic of calculating the new values is duplicated among all the requirement.
  • The list of data structures must be updated by the new field values. That's why Lens is used.

Solution:

Defines a sharing function that calculates the new values. Details can be referred to https://github.com/jinilover/apply-lens/blob/master/src/lib/Parliament/Utils.hs

adjustList :: Int -> Lens' a Int -> [a] -> [a]

Build the lenses of the involved data types

makeLenses ''CategoryFund
makeLenses ''District
makeLenses ''Contribution
makeLenses ''FinalBill

Then the FinalBill solution can be implemented by using adjustList. Details can be referred to https://github.com/jinilover/apply-lens/blob/master/src/lib/Parliament/FundDistribution.hs

b & (contributeFrom %~ adjustList _requiredFund contribute)
-- b is b@FinalBill{..}

Similarly, implement the District solution by using adjustList. Details can be referred to https://github.com/jinilover/apply-lens/blob/master/src/lib/Parliament/StructureBuildup.hs

d & categoryFunds %~ adjustList _availableFund maxAmount
  & categoryFunds %~ adjustList _availableFund defaultAmount
-- d is d@District{..}

adjustList can also be used to recalculate an integer list. Because Lens' a Int is type alias of

forall (f :: * -> *). Functor f => (Int -> f Int) -> a -> f a

Therefore identity is Lens' Int Int such that adjustList 100 identity [1,2,3,4] also works.

Appendix

Build and run the application

Enter the nix shell by nix-shell

To build the application

cabal build

To execute the application

cabal run apply-lens -- test/resources/parliament8.json report.json

To run the test-suite

cabal new-test test:tests

References

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors