joneshf / purescript-option

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

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.