Code Monkey home page Code Monkey logo

elm-pivot's Introduction

Pivot, the data structure

Pivot upgrades a list to a list with a center, left and right. You can think of it as a list with a cursor pointing at some value inside it, or better yet, a list with a location acting as a pivot. This structure makes a lot of sense when you want functionality such as

  • scrolling through a list, e.g. a playlist,
  • undo and redo, where the left holds previous states, and the right holds future states (after you did some undos),
  • control focus of form elements (although you'd need to be clever about it, to account for when no element is in focus),
  • and many more.

What if my list is empty?

Alright, slight lie earlier. Pivot actually upgrades a cons list (a non-empty list). You can try to upgrade a list, but it may fail. Explicitly, consider the following functions from this library,

from : List a -> Maybe (Pivot a)
fromCons : a -> List a -> Pivot a

Examples

Browsing

A pivot is perfect for implementing a collection browser (e.g. of a music playlist).

import Pivot as P exposing (Pivot)

type alias Model = Maybe (Pivot Artist)

type alias Artist = String

-- We start without any artists.
init : Model
init =
  Nothing

type Msg
  = Next
  | Previous
  | Remove
  | Add Artist
  | MoveItemUp
  | MoveItemDown
  -- etc...

update : Msg -> Model -> Model
update msg model =
  case msg of
    Next ->
      model
      -- Attempt to go forward, rollback if can't.
      |> Maybe.map (P.withRollback P.goR)
    Previous ->
      model
      -- Attempt to go back, rollback if can't.
      |> Maybe.map (P.withRollback P.goL)
    Remove ->
      -- Attempt to remove and go backwards.
      -- Will fail if there is no backwards to go to.
      case model |> Maybe.andThen P.removeGoL of
        Just collection ->
          Just collection
        Nothing ->
          -- Attempt to remove and go forward otherwise.
          -- Upon failure, there is nowhere to go, so Nothing is OK.
          model |> Maybe.andThen P.removeGoR
    Add item ->
      model
      -- Attempt to append to an existing collection.
      |> Maybe.map (P.appendGoR item)
      -- Start from scratch otherwise.
      |> Maybe.withDefault (P.singleton item)
      -- Wrap back into a `Maybe`.
      |> Just
    MoveItemUp ->
      model
      -- Attempt to move item forward.
      -- If it can't (no item in front), do nothing.
      |> Maybe.map (P.withRollback P.switchR)
    MoveItemDown ->
      model
      -- Attempt to move item backwards.
      -- If it can't (no item behind), do nothing.
      |> Maybe.map (P.withRollback P.switchL)
    -- etc...

Focus

Can you imagine how this library can be used to hold a bunch of input fields' values, with one of the fields in focus? Thinking about the fields as a collection, it is exactly like the browser from before.

Undo

This library can be used to add an undo-redo functionality to your app. Consider an simple app like this:

type alias Model =
  { counter1 : Int
  , counter2 : Int
  }

init : Model
init =
  { counter1 = 0
  , counter2 = 0
  }

type Msg
  = NoOp
  | Inc1
  | Inc2

update : Msg -> Model -> Model
update msg model =
  case msg of
    Inc1 ->
      { model | counter1 = model.counter1 + 1 }
    Inc2 ->
      { model | counter2 = model.counter2 + 1 }
    NoOp ->
      model

We decide that we want the user to be able to undo changes to the counters. To accomplish this, we append each new version of the model to a pivot instead of modifying it in place. This way we retain previous models, and can simply browse between them back and forth.

import Pivot as P exposing (Pivot)

-- This was the model before.
type alias Counters =
  { counter1 : Int
  , counter2 : Int
  }

initRec : Counters
initRec =
  { counter1 = 0
  , counter2 = 0
  }

type RecMsg
  = NoOp
  | Inc1
  | Inc2

-- This was the update function before.
updateRec : RecMsg -> Counters -> Counters
updateRec msgRec rec =
  case msgRec of
    Inc1 ->
      { rec | counter1 = rec.counter1 + 1 }
    Inc2 ->
      { rec | counter2 = rec.counter2 + 1 }
    NoOp ->
      rec

-- Now we wrap it all in a pivot.
type alias Model = Pivot Counters

init : Model
init =
  initRec
  |> P.singleton

type Msg
  = New RecMsg
  | Undo
  | Redo

update : Msg -> Model -> Model
update msg model =
  case msg of
    Undo ->
      model
      -- Try to undo.
      -- If there's no previous state, do nothing.
      |> P.withRollback P.goL
    Redo ->
      model
      -- Try to undo.
      -- If there's no next state, do nothing.
      |> P.withRollback P.goR
    New msgRec ->
      let
        next =
          -- Getting the current state.
          P.getC model
          -- Updating from it using the message.
          |> updateRec msgRec
      in
        model
        -- Adding the next state (instead of replacing the current one).
        |> P.appendGoR next

So we have undo, but we're recording every single state of the model. What if we only want part of the model to be undoable, say just the first counter? Below is just one possible implementation. I'll let you to try to make sense of it yourself.

import Pivot as P exposing (Pivot)

type alias Counter1Model = Pivot Int

type alias Model =
  { counter1 : Counter1Model
  , counter2 : Int
  }

init : Model
init =
  { counter1 = P.singleton 0
  , counter2 = 0
  }

type Msg
  = Counter1 Counter1Msg
  | Counter2

update : Msg -> Model -> Model
update msg model =
  case msg of
    Counter1 counter1Msg ->
      { model
      | counter1 =
        model.counter1
        |> counter1Update counter1Msg
      }
    Counter2 ->
      { model
      | counter2 =
        model.counter2 + 1
      }

type Counter1Msg
  = Inc
  | Undo
  | Redo

counter1Update : Counter1Msg -> Counter1Model -> Counter1Model
counter1Update counter1Msg counter1 =
  case counter1Msg of
    Inc ->
      let
        next =
          P.getC counter1 + 1
      in
        counter1
        |> P.appendGoR next
    Undo ->
      counter1
      |> P.withRollback P.goL
    Redo ->
      counter1
      |> P.withRollback P.goR

Alternatives

There are a few alternatives to this package.

elm-pivot's People

Contributors

lightandlight avatar martinos avatar mpizenberg avatar toastal avatar yotamdvir avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar

elm-pivot's Issues

`setR` reverses its argument

setR reverses its argument. For example, setR [4, 5, 6] [ 1 * 2 * 3 ] == [ 1 * 2 * 6 5 4 ].

Would you like me to submit a fix?

Attached: a property test suite that helped me diagnose the issue

module Tests.Pivot exposing (pivotTests)

import Expect
import Fuzz exposing (Fuzzer)
import Pivot exposing (Pivot)
import Test exposing (Test, describe)


genPivotInt : Fuzzer (Pivot Int)
genPivotInt =
    Fuzz.map3
        (\a b c -> Pivot.singleton b |> Pivot.setL a |> Pivot.setR c)
        (Fuzz.list Fuzz.int)
        Fuzz.int
        (Fuzz.list Fuzz.int)


genListInt : Fuzzer (List Int)
genListInt =
    Fuzz.list Fuzz.int


pivotTests : Test
pivotTests =
    describe "pivot"
        [ Test.fuzz genPivotInt "setL (getL a) == a" <|
            \pivot ->
                Pivot.setL (Pivot.getL pivot) pivot
                    |> Expect.equal pivot
        , Test.fuzz2 genListInt genPivotInt "getL (setL l a) == l" <|
            \list pivot ->
                Pivot.getL (Pivot.setL list pivot)
                    |> Expect.equal list
        , Test.fuzz genPivotInt "setR (getR a) == a" <|
            \pivot ->
                Pivot.setR (Pivot.getR pivot) pivot
                    |> Expect.equal pivot
        , Test.fuzz2 genListInt genPivotInt "getR (setR l a) == l" <|
            \list pivot ->
                Pivot.getR (Pivot.setR list pivot)
                    |> Expect.equal list
        , Test.fuzz3 Fuzz.int genListInt genPivotInt "goR (setR (x :: xs) a) == Just (setR xs (appendGoR x a))" <|
            \x xs pivot ->
                Pivot.goR (Pivot.setR (x :: xs) pivot)
                    |> Expect.equal (Just (Pivot.setR xs (Pivot.appendGoR x pivot)))

        ]

Optimize out abstractions

It appears that the uses of abstractions such as function calls, the function mirror, etc impact performance even though they are functionally equivalent to other more direct implementations (see discussion in #13).

Discrepancy between `mapCLR_` and `mapR_`.

See example below. For readability, list notation with center denoted by *s is used for a pivot.

mapR_ (List.take 1) [1,2,*3*,4,5] == [1,2,*3*,5]
mapCLR_ identity identity (List.take 1) [1,2,*3*,4,5] == [1,2,*3*,4]

Awkward implementation and lack of testing are to blame.

elm-pivot/Pivot/Map.elm

Lines 70 to 73 in cee1b98

mapR_ : (List a -> List a) -> Pivot a -> Pivot a
mapR_ f =
mapL_ f
|> mirror

Documentation makes matters worse, as it hints that these two applications above should have the same result.

elm-pivot/Pivot.elm

Lines 530 to 532 in cee1b98

{-| See `mapCLR_`.
-}
mapR_ : (List a -> List a) -> Pivot a -> Pivot a

As for fixing, the following works:

mapR_ = mapCLR_ identity identity

Still, it could be optimized, as mapCLR_ internally reverses the left side twice.

Test coverage

Tests should cover the entire library.

  • Every function should be tested against a typical example and all edge examples.
  • Every functional identity mentioned in the documentation should be fuzzy tested.

Indexed Maps

indexedMapCLR would be really nice right about now in my project. ...Or at least an easy way to zip a tuple of ( Int, a ).

Should we have a relative version of `zip`?

This is zip as it is now:

elm-pivot/Pivot/Map.elm

Lines 81 to 96 in cee1b98

zip : Pivot a -> Pivot ( Int, a )
zip pvt =
let
n =
lengthL pvt
onC =
(,) n
onL =
List.indexedMap (,)
onR =
List.indexedMap ((+) (n + 1) >> (,))
in
mapCLR_ onC onL onR pvt

It indexes the pivot like a list, without any regard to where the center is.
It may be beneficial to have a relative version.

 zipRelative : Pivot a -> Pivot ( Int, a ) 
 zipRelative pvt = 
     let 
         onC = 
             \x -> (0,x) 
  
         onL = 
             List.indexedMap (\i x -> (-1 - i,x)) 
  
         onR = 
             List.indexedMap (\i x -> (i + 1, x)) 
     in 
mapCLR_ onC onL onR pvt 

For example, a situation where one knows their pivot is going to get jumbled, and would like to retain their current positions afterwards.

In such a case we could also change the name of zip to zipAbsolute, leaving less room for confusion. To increase uniformity, we could further replace goTo by goAbsolute; and goBy by goRelative.

In fact, why is it called zip in the first place? index is better...

Upgrade to Elm v0.18

The only changes I see to be had are removing ' primes from variables. I'd also suggest renaming pure to singleton to match other Elm libraries.

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.