Coder Social home page Coder Social logo

arturopala / elm-monocle Goto Github PK

View Code? Open in Web Editor NEW
155.0 3.0 9.0 92 KB

Functional abstractions to manipulate complex records in Elm - Iso, Prism, Lens, Optional, Traversal.

License: MIT License

Elm 100.00%
elm-lang elm monocle lenses elm-monocle lens prism manipulate-complex-records traversal

elm-monocle's Introduction

Build Status

elm-monocle

A Monocle-inspired library providing purely functional abstractions to manipulate complex records in the elm language.

Published as arturopala/elm-monocle library.

Long Example

import Monocle.Optional exposing (Optional)
import Monocle.Lens exposing (Lens)


type StreetType
    = Street
    | Avenue


type Country
    = US
    | UK
    | FI
    | PL
    | DE


type alias Address =
    { streetName : String
    , streetType : StreetType
    , floor : Maybe Int
    , town : String
    , region : Maybe String
    , postcode : String
    , country : Country
    }


type alias Place =
    { name : String
    , description : Maybe String
    , address : Maybe Address
    }


addressOfPlace : Optional Place Address
addressOfPlace =
    Optional .address (\b a -> { a | address = Just b })


regionOfAddress : Optional Address String
regionOfAddress =
    Optional .region (\b a -> { a | region = Just b })


streetNameOfAddress : Lens Address String
streetNameOfAddress =
    Lens .streetName (\b a -> { a | streetName = b })


regionOfPlace : Optional Place String
regionOfPlace =
    addressOfPlace |> Monocle.Compose.optionalWithOptional regionOfAddress


streetNameOfPlace : Optional Place String
streetNameOfPlace =
    addressOfPlace |> Monocle.Compose.optionalWithLens streetNameOfAddress


place : Place
place =
    { name = "MyPlace"
    , description = Nothing
    , address =
        Just
            { streetName = "Union Road"
            , streetType = Street
            , floor = Nothing
            , town = "Daisytown"
            , region = Nothing
            , postcode = "00100"
            , country = US
            }
    }


updatedPlace : Place
updatedPlace =
    place
        |> regionOfPlace.set "NorthEast"
        |> streetNameOfPlace.set "Union Avenue"

Abstractions

Iso

An Iso is a tool which converts elements of type A into elements of type B and back without loss.

    type alias Iso a b =
        { get : a -> b
        , reverseGet : b -> a
        }
Example
    string2CharListIso : Iso String (List Char)
    string2CharListIso =
        Iso String.toList String.fromList

    (string2CharListIso.get "ABcdE") == ['A','B','c','d','E']
    (string2CharListIso.reverseGet ['A','B','c','d','E']) == "ABcdE"

Prism

A Prism is a tool which optionally converts elements of type A into elements of type B and back.

    type alias Prism a b =
        { getOption : a -> Maybe b
        , reverseGet : b -> a
        }
Example
    string2IntPrism : Prism String Int
    string2IntPrism =
        Prism String.toInt String.fromInt

    string2IntPrism.getOption "17896" == Just 17896
    string2IntPrism.getOption "1a896" == Nothing
    string2IntPrism.reverseGet 1626767 = "1626767"

Lens

A Lens is a functional concept which solves a very common problem: how to easily update a complex immutable structure, for this purpose Lens acts as a zoom into a record.

    type alias Lens a b =
        { get : a -> b
        , set : b -> a -> a
        }
Example
    type alias Address = 
        { streetName: String
        , postcode: String
        , town: String
        }

    type alias Place =
        { name: String
        , address: Address
        }

    addressStreetNameLens : Lens Address String
    addressStreetNameLens =
        Lens .streetName (\b a -> { a | streetName = b })

    placeAddressLens : Lens Place Address
    placeAddressLens =
        Lens .address (\b a -> { a | address = b })

    placeStreetName: Lens Place String
    placeStreetName =
        placeAddressLens |> Monocle.Compose.lensWithLens addressStreetNameLens

    myPlace = Place "my" (Address "Elm" "00001" "Daisytown")
    placeStreetName.get myPlace == "Elm"
    
    myNewPlace = placeStreetName.set "Oak" myPlace

    placeStreetName.get myNewPlace == "Oak"
    myNewPlace == Place "my" (Address "Oak" "00001" "Daisytown")

Optional

A Optional is a weaker Lens and a weaker Prism.

    type alias Optional a b =
        { getOption : a -> Maybe b
        , set : b -> a -> a
        }
Example
    addressRegionOptional : Optional Address String
    addressRegionOptional =
        Optional .region (\b a -> { a | region = Just b })

    string2IntPrism : Prism String Int
    string2IntPrism = Prism String.toInt String.fromInt

    addressRegionIntOptional: Optional Address Int
    addressRegionIntOptional =
        addressRegionOptional |> Monocle.Compose.optionalWithPrism string2IntPrism

    string2CharListIso : Iso String (List Char)
    string2CharListIso = Iso String.toList String.fromList

    addressRegionListCharOptional: Optional Address (List Char)
    addressRegionListCharOptional =
        addressRegionOptional |> Monocle.Compose.optionalWithIso string2CharListIso

    modifyRegion: String -> String
    modifyRegion region = String.reverse region

    modifyAddressRegion: Address -> Maybe Address
    modifyAddressRegion address = Optional.modifyOption addressRegionOptional modifyRegion address

    modifyRegion: String -> String
    modifyRegion region = String.reverse region

    modifyAddressRegion: Address -> Address
    modifyAddressRegion address = Optional.modify addressRegionOptional modifyRegion address

Traversal

A Traversal allows you to modify many elements at once.

    type alias Traversal a b =
        (b -> b) -> a -> a

(Traversal a b is just an alias for a function that applies a transformation over b elements of a larger a structure.)

Example
    firstNameLens : Lens Friend String
    firstNameLens =
        Lens .firstName (\b a -> { a | firstName = b })

    bestFriendsTraversal : Traversal (List Friend) Friend
    bestFriendsTraversal =
        Traversal.some
            Traversal.list
            (\friend -> friend.value == Best)

    friendsLens : Lens Account (List Friend)
    friendsLens =
        Lens .friends (\b a -> { a | friends = b })

    firstNamesOfBestFriends : Traversal Account String
    firstNamesOfBestFriends =
        friendsLens
            |> Compose.lensWithTraversal bestFriendsTraversal
            |> Compose.traversalWithLens firstNameLens

    upcaseBestFriendsFirstNames : Account -> Account
    upcaseBestFriendsFirstNames account =
        Traversal.modify firstNamesOfBestFriends String.toUpper

Common

Common lenses/prisms/optionals that most projects will use.

Step into a Maybe value.

    maybe.set 5 Nothing
    > Just 5

Step into an Array at the given index.

    .getOption (array 2) (Array.fromList [ 10, 11, 12, 13 ])
    > Just 12

    .getOption (array 8) (Array.fromList [ 10, 11, 12, 13 ])
    > Nothing

Step into a Dict with the given key.

    .getOption (dict "Tom") (Dict.fromList [ ( "Tom", "Cat" ) ])
    > Just "Cat"

    .getOption (dict "Jerry") (Dict.fromList [ ( "Tom", "Cat" ) ])
    > Nothing

Step into the success value of a Result.

    result.getOption (Ok 5)
    > Just 5

    result.getOption (Err "500")
    > Nothing

Step into a record with an id key.

Since records with an id field are incredible common, this is included for convenience. It also serves as a simple recipe for creating record lenses.

    id.get { id = 1000, name = ... }
    > 1000

Step into the first element of a pair.

    first.get ( 'a', 'b' )
    > 'a'

Step into the second element of a pair.

    second.get ( 'a', 'b' )
    > 'b'

Build

Prerequisites

  • Node.js
  • Yarn
  • Run yarn install-with-elm

Compile

Run yarn compile

Test

Run elm-test

elm-monocle's People

Contributors

aische avatar andys8 avatar arturopala avatar bastes avatar crianonim avatar krisajenkins avatar kylecorbelli avatar lue-bird avatar toastal avatar zaboco avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

elm-monocle's Issues

Consider adding some extremely common optionals?

This library is brilliant - thanks for creating it!

I find myself using a lot of common optionals again & again - things like:

dict : comparable -> Optional (Dict comparable v) v
dict key =
    { getOption = Dict.get key
    , set = Dict.insert key
    }

So far I have a fistful for things like Array/Maybe/Dict/Result & first/second/.id.

I'd like to publish them somewhere so I can reuse them. This might be the right home. Would you like to add them here? If so, I'll PR. :-)

Thanks again!

Examples for Common.dict and Common.array do not seem to work

Here's the Dict example:

screen shot 2016-12-01 at 3 45 05 pm

Here's what happens when I run the example:

850 $ elm-repl
---- elm-repl 0.17.1 -----------------------------------------------------------
 :help for help, :exit to exit, more at <https://github.com/elm-lang/elm-repl>
--------------------------------------------------------------------------------
> import Monocle.Common exposing (..)
> import Dict
> dict.getOption "Tom" (Dict.fromList [("Tom","Cat")])
-- TYPE MISMATCH --------------------------------------------- repl-temp-000.elm

`Monocle.Common.dict` is being used in an unexpected way.

5|   dict.getOption "Tom" (Dict.fromList [("Tom","Cat")])
     ^^^^
Based on its definition, `Monocle.Common.dict` has this type:

    comparable -> Monocle.Optional.Optional (Dict.Dict comparable c) c

But you are trying to use it as:

    { b | getOption : a }

(I'm using 1.3.0, but the example is the same in that version and in 1.4.)

Infix operator alias for Lens.compose

I really love monocle and the ability to use lenses to get and set values easily.

But since lenses are especially interesting when you can compose them, and since composition can be deep for deeply nested structures, it's a bit annoying and counter-intuitive to have to do this:

lens1 : Lens a b
lens2 : Lens b c
lens3 : Lens c d

lens13_1 : Lens a d
lens13_1 =
  lens3
    |> compose lens2
    |> compose lens1

-- OR

lens13_2 : Lens a d
lens13_2 =
  compose (compose lens1 lens2) lens3

-- OR

lens13_3 : Lens a d
lens13_3 =
  let
    lens12 = compose lens1 lens2
  in
    compose lens12 lens3

-- OR EVEN
lens13_4 : Lens a d
lens13_4 =
  compose lens1 <| compose lens2 lens3

All these patterns are grating to read. We usually solved it making "half-way" lenses like:

lens12 = compose lens1 lens2
lens13 = compose lens12 lens3

But I can't help thinking we might benefit from an infix Lens.compose alias allowing to do things like:

(<|>) = Lens.compose
lens13 = lens1 <|> lens2 <|> lens3

(I've actually done it in projects of my own that use lenses)

So, what do you think?

Elm v0.19

With the removal of operators, I guess we're going to need some cleanup.

Would you like some help? If so, how do you think we should proceed? :)

Crazy idea: methods for pipeline-style composition (to replace operators)

Hey :) my mistaken proposal to help might give something interesting after all, a crazy idea, but it might just work :p

So, composing with operators was cool because it was terse and readable (once you knew what the operator did) and it was almost as cool as "real" lens composition with the function composition operator (a la haskell).

And now we can't have nice toys. Thanks Elm 0.19!

But, what if we used the pipeline operator (|>) with an appropriately named function to chain composition?

So, here's the style we could be using:

-- with :
-- lab => Lens A B
-- lbc => Lens B C
-- ...
-- oab => Optional A B
-- obc => Optional B C
-- ...

lad : Lens A D
lad =
  lab
    |> Lens.andLens lbc
    |> Lens.andLens lcd

oad : Optional A D
oad =
  lab
    |> Lens.andOptional obc
    |> Optional.andOptional lcd

So, each composition method could be found in their "starting" optic's module (more precisely, in the module of the type of optic of their second argument) and would be named after the type of the optic used in the composition (the type of optic in the first argument).

-- Lens
andLens : Lens B C -> Lens A B -> Lens A C
...

andOptional : Optional B C -> Lens A B -> Optional A C
...

-- Optional
andLens : Optional B C -> Lens A B -> Optional A C
...

andOptional : Optional B C -> Optional A B -> Optional A C
...

I think you get the idea (in fact, you probably have gotten it just by reading the title, but I'm writing my thoughts as they come here mostly :p). I think this is as good (and readable, and intuitive) as we could make it without typeclasses, and it'd work quite well with the ideology of discoverability and clarity Elm is pushing with the restriction of defining operators now.

So, what do you think?

If you're game, I'm going to prepare a pull request implementing it at least for Lens & Optionals as a proof of concept (well, I think I'm going to do it anyways ;p but I'd still like your input about that idea and its ramifications ; maybe there's some roadblock ahead I'm not seeing right now).

Traversal

Hi @arturopala :)

I've been toying with the idea of adding Traversal here for quite some time, and I've just bumped into a case where it'd be useful. So here's me asking whether you'd like a PR that'd add the a new Monocle.Traversal module (and composition functions in Compose) :) (I haven't started on it yet, and since I'm going to prepare this from my use case I'm open to your thoughts on what should be in it and how).

So, what do you say?

Is the example for `Prism` correct?

Here's the example:

    string2IntPrism.getOption "17896" == Just 17896
    string2IntPrism.getOption "1a896" == Nothing
    string2IntPrism.reverseGet 1626767 = "1626767"

My reading of both the Scala Monocle prism laws and the Haskell prism laws suggest that doesn't work.

Here's the Scala description of the second law:

def partialRoundTripOneWay[S, A](p: Prism[S, A], a: A): Boolean =
  p.getOption(p.reverseGet(a)) == Some(a)

That suggests to me that getOption (reverseGet "1626767") == Just 1626767, but it will actually return Nothing.

The Haskell doc defines the equivalent as its first law:

First, if I `re` or `review` a value with a Prism and then `preview` or use (^?), I will get it back:

preview l (review l b)  Just b

Your implementation is arguably more useful, since it seems the law restricts you to a sum type.

I'm probably confused.

Error on install

> elm package install arturopala/elm-monocle
To install arturopala/elm-monocle I would like to add the following
dependency to elm-package.json:

    "arturopala/elm-monocle": "1.2.0 <= v < 2.0.0"

May I add that to elm-package.json for you? [Y/n] y

Error: I cannot find a set of packages that works with your constraints.

--> There are no versions of arturopala/elm-monocle that work with Elm 0.17.1.
    Maybe the maintainer has not updated it yet.

Implementation specifics question

Hi @arturopala thanks you for such a nice library!

Can you, please, tell me why you chose to use records with functions as properties instead of using opaque type and functions like Monocle.get, Monocle.set, like for example, in Focus library?
I am trying to understand if it was just a style preference or there was more to it :)

Merge Prism and Optional

I'm failing to see the benefit from isolation of those two abstractions. Both serve the same purpose and Prisms end up being Optional, once composed any way. Also there's no such concept as Optional in the classical lens libraries in Haskell, so it's a source of confusion for people with such background (I know it was for me).

So I suggest not to breed concepts unless you have a good reason and remove the current implementation of Prism and rename Optional to Prism.

Optional for list too

This is more a question than an issue or request, but why is there an "array" to access an item at some place in an array and not the equivalent list? Is this an optimization thing or just an edge case that wasn't as urgent as others?

Since I feel it can be interesting to have, I'd be willing to add it to the codebase in a PR, unless there is a good reason for it not to exist :)

Example in README doesn't work, no operator "=>"

The following example in README doesn't work. I also tried changing => to >> in case it was just a typo, but it still doesn't compile, just gives a lof of type errors.

regionOfPlace : Optional Place String
regionOfPlace =
    addressOfPlace => regionOfAddress

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.