-
Notifications
You must be signed in to change notification settings - Fork 16
Description
The idea
I was talking to @gabejohnson the other day about using purescript-option to solve a problem. It came up that it'd be ideal if we could take an Option.Option _, and convert it to Data.Maybe.Just _ if any of the values exist or Data.Maybe.Nothing if none of them do. @gabejohnson mentioned that it'd be akin to Option.getAll, but only return Data.Maybe.Nothing if no values are there (as opposed to at least one value not being there). @gabejohnson also suggested Option.getSome as the name of the value.
The problem
While it seemed feasible at first, I think it's a value that would end up being hard to use. An example might help. Let's say we have:
type Greeting
= Option.Option
( name :: String
, title :: String
)What we want is a family of functions:
getSome ::
Greeting ->
Data.Maybe.Maybe
{
}
getSome ::
Greeting ->
Data.Maybe.Maybe
{ name :: String
}
getSome ::
Greeting ->
Data.Maybe.Maybe
{ title :: String
}
getSome ::
Greeting ->
Data.Maybe.Maybe
{ name :: String
, title :: String
}We can probably write a typeclass and instance(s) for this family of functions. The hard part is using it. If we were to say:
greet ::
Greeting ->
Data.Maybe.Maybe ?option
greet option = Option.getSome optionWhat would we expect ?option to be? There's four valid choices for it, and if we choose the wrong one, we'll might get a Data.Maybe.Nothing when we didn't expect it . That's not really what we wanted. We wanted to take any Option.Option _, and only get a Data.Maybe.Nothing if none of the values were there.
The real implementation?
It almost seems like what we want is something like:
getSome ::
Greeting ->
Data.Maybe.Maybe
( Data.Variant.Variant
( name :: String
, name_title ::
{ name :: String
, title :: String
}
, title :: String
)
)This way, we'll at least have a single type that is always the same. You'd be able to discriminate the cases dynamically instead of having to take a guess statically.
An alternative implementation
Instead of creating that family of functions, we can throw more Data.Maybe.Maybe _s in the mix and have a single function:
getSome ::
Greeting ->
Maybe
{ name :: Maybe String
, title :: Maybe String
}I think this is actually more inline with the specific example @gabejohnson was dealing with. That seems like something we could throw together immediately as:
getSome ::
forall option record.
ToRecord option record =>
Option option ->
Data.Maybe.Maybe (Record record)
getSome option@(Option object)
| Foreign.Object.isEmpty object = Data.Maybe.Nothing
| otherwise = Data.Maybe.Just (toRecord option)We can open up the Option.Option _ and check if the underlying Foreign.Object.Object _ is empty. If it is, there's no values, so we can return Data.Maybe.Nothing. Otherwise, we grab what we can.
My main qualm with this implementation is that anyone consuming it still has to handle the Data.Maybe.Just { name: Data.Maybe.Nothing, title: Data.Maybe.Nothing } case. That doesn't seem like it should be a possible value, but the types still allow it.
The current workaround
Assuming the Option.Option _ can have a Data.Eq.Eq _ instance, there's a way to get the alternative implementation without adding something to the Option module. Anyone can write the following value external to the Option module:
getSome ::
forall option record.
Data.Eq.Eq (Option.Option option) =>
Option.ToRecord option record =>
Option.Option option ->
Data.Maybe.Maybe (Record record)
getSome option
| option == Option.empty = Data.Maybe.Nothing
| otherwise = Data.Maybe.Just (Option.toRecord option)If the Option.Option _ has values without a Data.Eq.Eq _ instance, they can't use that value. It's not an end-all, but it should unstick while we try to figure things out here.
What to do?
I'd like to sit on this for a while. Each of the implementations above come with their own set of issues: hard to choose a type, illegal states represented, additional constraints.