Skip to content

Instantly share code, notes, and snippets.

@i-am-tom
Created December 14, 2017 21:59
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save i-am-tom/1036ccaf15d5b8f31e4d1107c1bfa4de to your computer and use it in GitHub Desktop.
Save i-am-tom/1036ccaf15d5b8f31e4d1107c1bfa4de to your computer and use it in GitHub Desktop.
Generating unique identifiers with a purely functional approach.
module Main where
-- Often in applications, we want to generate unique IDs. In imperative
-- programming, we might be tempted to define, albeit with obfuscation,
-- a global integer that we increment every time we want a new value.
-- In this gist, we'll look at how to achieve the same thing using the
-- state type to keep things good and pure.
import Control.Apply (lift2)
import Control.Monad.Eff (Eff)
import Control.Monad.Eff.Console (log)
import Control.Monad.State (State, evalState, get, modify)
import Data.Foldable (fold)
import Data.Tuple (Tuple(Tuple))
import TryPureScript (render, withConsole)
import Prelude
-- We're going to be "mutating state", right? So, all we're really doing
-- is making a version of State where the "state" is always an integer.
-- It really is that straightforward!
newtype Identifier a = Identifier (State Int a)
-- Because this is just a `newtype` over `State`, we can get a _lot_ of
-- help from PureScript. All our instances are automatically derivable:
derive newtype instance functorIdentifier :: Functor Identifier
derive newtype instance applyIdentifier :: Apply Identifier
derive newtype instance applicativeIdentifier :: Applicative Identifier
derive newtype instance bindIdentifier :: Bind Identifier
derive newtype instance monadIdentifier :: Monad Identifier
-- The trick here is that we're not going to expose the constructor for
-- Identifier, so nothing can deconstruct the type. Because of that, a
-- user of the type can only produce `Identifier` values through `pure`
-- and `fresh`, which we'll see below is enough to ensure that values
-- stay unique.
---
-- I think this is a really pretty expression. A "fresh value" is a
-- `State` value containing the updated integer in both positions.
-- This means that, every time you `bind` on a `fresh`, the value
-- returned is a unique identifier!
fresh :: Identifier Int
fresh = Identifier (modify (_ + 1) *> get)
-- When we've built up our full computation and the time comes to
-- run it, producing fresh identifiers as we go, all we actually do
-- is evaluate the computation using `0` as the initial state. Every
-- time `fresh` is used, that state is incremented, so we're good!
runIdentifier :: forall a. Identifier a -> a
runIdentifier (Identifier state) = evalState state 0
---
-- Here's a program that needs a unique identifier.
firstProgram :: Identifier String
firstProgram = fresh <#> \id ->
"We have ID #" <> show id
-- Here's another program that needs a couple of unique IDs. If
-- all is well, combining these two programs should make the IDs
-- unique across both!
secondProgram :: Identifier String
secondProgram = do
one <- fresh
two <- fresh
pure $ fold
[ "We, on the other hand, have ID #"
, show one
, " AND ID #"
, show two
]
-- Here's a really boring way of combining those programs...
combined :: Identifier (Tuple String String)
combined = lift2 Tuple firstProgram secondProgram
-- And, finally, we use `runIdentifier` to generate unique IDs over
-- both programs, perform the computations, and return the results!
main :: Eff _ Unit
main = render <=< withConsole $ do
let (Tuple first second) = runIdentifier combined
-- Ta-da!
log first
log second
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment