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
Many functional languages offer record types. These behave similarly to JS objects.
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
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
}
// 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
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