The other day, I wrote about handling JS Unions with Row Types, by using a constructorless data type with a row parameter to note what labels I want to associate members with, with guard functions for how you would unsafely guard on these types. The unsafeGuardMember
function described in that post gave us a way to eliminate members of the union until we are ultimately left with no more tracked union members (which very well happens in many uses of JS Unions).
This time, I'll continue with just a small extension to implement a match
function.
Short review
Before we start, let's review the definitions of JSUnion
and UnsafeGuardFor
.
data JSUnion (members :: # Type)
Purposefully, the JSUnion cannot be constructed directly, but the members
parameter being of kind # Type
gives us a way to work with the possible values inside the union.
newtype UnsafeGuardFor (name :: Symbol) ty =
UnsafeGuardFor (Foreign -> Boolean)
For a given field in our JSUnion, it should be feasible to implement a guard function that explicitly declares what type is to be extracted, using a test of Foreign -> Boolean
.
class MatchMembers
With this information, we can define a class where we can iterate the members
row type using RowToList
. We can define this in terms of the RowList
being iterated, members
of the JS Union, a record of pairs
that will contain the guard and an associated function to be applied, and a result
type for the result of the functions being applied in the matching. And since we know that we may not have enough information to exhaustively check for the JS Union members (as is the case with any dynamically typed "outside world" value), the class method should return a Maybe result
, where the Nothing will be returned if nothing has been found.
Put into a class definition, we end up with this:
class MatchMembers (xs :: RowList) (members :: # Type) (pairs :: # Type) result
| xs result -> members pairs
where
matchMembers :: RLProxy xs -> { | pairs } -> JSUnion members -> Maybe result
Where the fundep xs result -> members pairs
exists primarily because row types can't be used in instance heads for matching. Otherwise, everything else is in order here.
I should probably mention here that maybe you actually are sure that you have exhaustively listed the possible members of your JS Union. In that case, you may want to use a coerced fromJust
function, but it's your fault if it breaks.
Then our instances put concretely what we have described above. Starting with Nil
:
instance matchMembersNil :: MatchMembers Nil members pairs result where
matchMembers _ _ _ = Nothing
matchMembersCons
As with most RowList
classes, our Cons
instance contains the meat of what we want to accomplish. The one decision that needs to be made here is what kind of concrete type we want the pairs
to have. I chose to go with the easy choice of using the Tuple
type. From there, the rest of the instance constraints fall in line, where from Cons name ty tail
, name
is declared as a known Symbol
, members
is declared to have name ty
as a field, and pairs
is declared to have a tuple of the guard and the result function:
instance matchMembersCons ::
( IsSymbol name
, RowCons name ty members' members
, RowCons name (Tuple (UnsafeGuardFor name ty) (ty -> result)) pairs' pairs
, MatchMembers tail members pairs result
) => MatchMembers (Cons name ty tail) members pairs result where
And as usual, the tail
is then used to continue the instances through the list. The method body then has nothing more than application of the unsafeGuardMember
function defined previously:
matchMembers _ pairs union =
case unsafeGuardMember unsafeGuard union of
Right x -> Just $ fn x
Left _ -> matchMembers (RLProxy :: RLProxy tail) pairs union
where
nameP = SProxy :: SProxy name
Tuple unsafeGuard fn = Record.get nameP pairs
And so using the RowCons
constraint for pairs
from above, we grab the Tuple unsafeGuard fn
from { | pairs }
.
matchJSUnion
Then all this needs is a nice top-level function to be called with, so we define it almost verbatim minus the RLProxy
argument that we can infer the type for:
matchJSUnion
:: forall members xs pairs result
. RowToList members xs
=> MatchMembers xs members pairs result
=> { | pairs }
-> JSUnion members
-> Maybe result
matchJSUnion =
matchMembers (RLProxy :: RLProxy xs)
And that's it!
Usage
First, let's see a usage that matches on a valid specified JS Union member.
T.test "matchMembers 1" do
let
(union :: TestUnion) = H.fromMember countP 1
match = H.matchJSUnion
{ count: Tuple countGuard show
, name: Tuple nameGuard id
}
union
case match of
Just value -> do
Assert.equal value "1"
Nothing ->
T.failure "incorrect result from matchJSUnion"
Cool, so this works as expected, where the count
guard correctly matches and we get Just "1""
as a result of show
on the integer 1
.
Next, let's see a typical case where some value doesn't match any of the cases we're handling.
T.test "matchMembers 2" do
let
(union :: TestUnion) = unsafeCoerce { crap: "some bullshit from JS" }
match = H.matchJSUnion
{ count: Tuple countGuard show
, name: Tuple nameGuard id
}
union
case match of
Just value -> do
T.failure "incorrect result from matchJSUnion"
Nothing ->
T.success
And so, we correctly get Nothing
as a result of not matching on anything here. And yes, while this example seems readily obvious, people forget about cases when "exhaustive" checks are not actually exhaustive, so this actually is quite important to have.
Otherwise, if you are truly working with a closed set of members, then you could use fromJust
here.
Conclusion
Hopefully this has shown that with a little bit of normal RowToList
usage, you can provide yourself more convenient and correct ways of modeling problems and working with them.
If you're at all interested in using this library also, ping me sometime and I'll be more active about publishing and maintaining it.
Links
- PureScript-Hotteok: https://github.com/justinwoo/purescript-hotteok
- Previous blog post, "Handling JS Unions with Row Types": https://qiita.com/kimagure/items/141423771ad1f5a84425