Skip to content

Instantly share code, notes, and snippets.

@toastal
Last active March 18, 2019 16:14
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 toastal/2d389b8c57dc1b1f038e5aee407cfbf3 to your computer and use it in GitHub Desktop.
Save toastal/2d389b8c57dc1b1f038e5aee407cfbf3 to your computer and use it in GitHub Desktop.

Row Things

In JavaScript objects (and sometimes strings *shudder at Redux Actions*) are the structure of choice for many tasks. They can hold functions, arrays, other objects, etc. They can be a part of a factory and the can be mutated to do just about anything. Despites its versatility, it doesn't do much in the ways of providing rest-assuredness. You might be able to freeze it, but you can't even do basic equality:

{little: "kitten"} == {little: "kitten"}
//=> false

Records

Many functional languages offer record types. These behave similarly to JS objects.

Elm

type alias Hero =
  { name : String
  , cape : Maybe Color
  } 


batman : Hero
batman =
  { name = "Bruce Name"
  , cape = Just Black
  }


-- Haskell-like shorthand
trueHero : Hero
trueHero =
  Hero "Hodor" Nothing

PureScript

type Hero =
  { name  String
  , cape  Maybe Color
  }
  
batman  Hero
batman =
  { name: "Bruce Wayne"
  , cape: Just Black
  }
  
-- no shorthand
trueHero  Hero
trueHero =
  { name: "Hodor"
  , cape: Nothing
  }

JavaScript

// No static types

batman = {
  name: "Bruce Wayne",
  cape: "black"
}

trueHero = {
  name: "Hodor",
  cape: null
}

// or do we just omit the cape?

trueHero_ = {
  name: "Hodor"
}

So whether or not to omit the key-value pair is a good question since it effectively represents the same thing. Not having it is probably preferable in an aesthetic way and in a sending-less-data-down-the-wire-in-a-JSON context. To use hypothetically use that value...

-- Elm
renderCape : Hero -> HTML msg
renderCape hero =
  case hero.cape of
    Nothing ->
      text ""
     
    Just c ->
      div [ style [ ( "backgroundColor", toColor c ) ] ] [ text "Cape" ]
-- PureScript
renderCape  Hero  HTML
renderCape = case _.cape of
  Nothing → text ""
  Just c → HH.div [ Hc.style do CSS.backgroundColor (toColor c) ] [ HH.text "Cape" ]
// JavaScript
//: ? -> HTML | null
const renderCape (hero) =>
  hero.cape == null
    ? null
    : h("div", {style: {backgroundColor: hero.cape}}, "Cape")

Now, I know that we could swing the JavaScript example with just hero.cape == null{.js} and allow the weak duck typing runtime to say "yup, nope... both cases are null in my eyes", but we're doing it this way to illustrate a point. This ability just see if a key exists can be kind of handy--and it's definitely useful in JS. In fact if you try to do this nested-like with state.hero.cape == null and hero is undefined, you will get a runtime error.

If for some reason you're curious one tail-recursive way to make a function that deals with this
//: ([String], { String, v }) -> v | null
const getPath = ([key, ...tail], obj) => {
  if (key == null || typeof obj !== "object" || !(key in obj)) {
    // key is null or the object isn't really an object or key isn't in object? 
    return null
  } else if (tail.length === 0) {
    // are we at the last item in the list?
    return obj[key]
  }
  return getPath(tail, obj[key])
}
getPath(["a"], {a: {b: true}})
//=> {b: true}

getPath(["a", "b"], {a: {b: true}})
//=> true

getPath(["a", "c"], {a: {b: true}})
//=> null

getPath(["a", "b", "c"], {a: {b: true}})
//=> null

So why bring this up?

The term-level flexibility of JS objects is one of the reasons people like JavaScript. It's not safe but it can be useful. What if we strictly didn't have a cape, but wanted to add a red one if and only if the cape was missing?

// JavaScript
const addCape = (color, hero) => {
  if (!("cape" in hero)) {
    // mwaha mutation
    hero.cape = color
  }
  return hero
}

addCape("red", {name: "Superman"})
//=> {name: "Superman", cape: "red"}
-- PureScript
import Data.Record as Record

-- Note: all heroes must now wear a cape
type Hero = { name  String, cape  Color }

-- That sweet curry
addCape   r. RowLacks "cape" r  Color  { name  String | r }  Hero
addCape =
  Record.insert "cape"
  
addCape Red { name: "Superman" }
--=> { name: "Superman", cape: Red }
-- Elm
-- Literally can't do this

JavaScript can do this because it actually doesn't need to make an sense and runtime errors, exceptions, etc. are the norm. But in a typed world, this is called row polymorphism or extensible records. Break this down a bit:

-- / Function name
-- |
-- |      / `forall` generic rs
-- |      |
-- |      |    / `RowLacks` type constraint (⇒) which basically insures 
-- |      |    | that "cape" is a row in the record given as the second
-- |      |    | argument
-- |      |    |
-- |      |    |                   / First argument: Color
-- |      |    |                   |
-- |      |    |                   |       / Second argument: some record
-- |      |    |                   |       | with `name ∷ String` in it…
-- |      |    |                   |       | that & not having a `cape`
-- |      |    |                   |       | is what we know
-- |      |    |                   |       |
-- |      |    |                   |       |  …& we'll get a hero out \
-- |      |    |                   |       |                          |
addCape   r. RowLacks "cape" r  Color  { name  String | r }  Hero

So what does this do? A

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment