Provide a way to get any values that exist in an `Option.Option _` or none at all
joneshf opened this issue · comments
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 option
What 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.