Either type-level list - Haskell

Welcome to the Functional Programming Zulip Chat Archive. You can join the chat here.

Sridhar Ratnakumar

Using Either as a type operator makes the signature more palatable for nested Eithers:

parse :: String -> Foo `Either` Bar `Either` Baz
parse = undefined

But writing Right (Left bar) is pretty janky. Can that be improved as well?

Sridhar Ratnakumar
class Way v e where
  this :: v -> e

instance Way a (a `Either` b `Either` c) where
  this = Left . Left

instance Way b (a `Either` b `Either` c) where
  this = Left . Right

instance Way c (a `Either` b `Either` c) where
  this = Right

parse :: Text -> Foo `Either` Bar `Either` Baz
parse = this (undefined :: Bar)
Sridhar Ratnakumar

Now can we do that in _reverse_? i.e., pattern match on the either values?

Asad Saeeduddin

@Sridhar Ratnakumar idk if you want to go this far, but take a look at: https://github.com/masaeedu/co-optics/blob/master/src/SOP/EOT.hs

Two optics for the price of one. Contribute to masaeedu/co-optics development by creating an account on GitHub.
Asad Saeeduddin

this lets you write stuff like:

data Value = N Number | S String | B Bool | Null | O Object | A Array
  deriving (Generic, Show)
instance SOP.Generic Value

-- Parse/print a JSON value (without leading/trailing whitespace)
jsonValue :: Biparser' Maybe Value
jsonValue = gsop $
  jsonNumber \/
  jsonString \/
  jsonBool   \/
  jsonNull   \/
  jsonObject \/
  jsonArray
Asad Saeeduddin

where (\/) :: Demux p => p a b -> p c d -> p (Either a c) (Either b d). assuming your parsers are functors instead of profunctors, you'll be building up nested eithers via the analogous Alternative class, which has union :: Alternative f => f a -> f b -> f (Either a b).

TheMatten

Data types รก la carte (http://www.cs.ru.nl/~W.Swierstra/Publications/DataTypesALaCarte.pdf, section 4) describes :<: constraint capturing "subtype" relation

Sridhar Ratnakumar

@Asad Saeeduddin But you still have to create a wrapper ADT (Value in your case), no?

Sridhar Ratnakumar

@TheMatten Interesting, but why does it need to be a functor? In my case they could be any types. I came up with this:

type (:+:) = Either

instance a :<: (a :+: b) where
  inj = Left

instance b :<: (a :+: b) where
  inj = Right

instance {-# OVERLAPPABLE #-} a :<: c => a :<: (c :+: b) where
  inj = Left . inj

x :: Int :+: String :+: Char :+: Text
x = inj ("hello" :: String)
Sridhar Ratnakumar

This supports arbitrarily nested Either, compared to the Way class above, which is nice.

Sridhar Ratnakumar

Wait, can Polysemy.Internal.Union be used here?

Sridhar Ratnakumar

But it is not general enough. Uses Effect

Sridhar Ratnakumar

And here's the pattern matching part.

type (:+:) = Either

instance a :<: (a :+: b) where
  inj = Left
  pick = either Just (const Nothing)

instance b :<: (a :+: b) where
  inj = Right
  pick = either (const Nothing) Just

instance {-# OVERLAPPABLE #-} a :<: c => a :<: (c :+: b) where
  inj = Left . inj
  pick = either pick (const Nothing)

pattern Member {thing} <-
  (pick -> Just thing)
  where
    Member = inj

mkStuff :: Int :+: String :+: Char :+: Text
mkStuff = Member ("hello" :: String)

useStuff :: Int :+: String :+: Char :+: Text -> String
useStuff = \case
  Member s -> s
  Member (n :: Int) -> show n
  _ -> error "No string"

Very nice!

Asad Saeeduddin

@Sridhar Ratnakumar The ADT isn't a wrapper, it's the end result I'm trying to get. The point is I can have a nested type level tree of tuples and eithers, and as long as its shape conforms to some generic datatype, i can just use a standard isomorphism gsop to convert it, without having to manually fiddle with all the lefts and rights and fsts and snds

Sridhar Ratnakumar

Not sure if it is possible to write a COMPLETE pragma for polymorphic pattern synonyms though ...

Sridhar Ratnakumar

Yea, it seems impossible to write a COMPLETE pragma for the Member pattern. Sheesh

Sridhar Ratnakumar

Thought I was getting close to arriving at a simple solution for the open union problem.

Sridhar Ratnakumar

I can suppress the warning with {-# COMPLETE Member :: Either #-} though, but that's not ideal of course.

Sridhar Ratnakumar

Asked for help here: https://discourse.haskell.org/t/writing-complete-pragma-for-polymorphic-pattern-synonyms/1198

I have arrived at this approach of making handling of nested Either's more elegant; it uses pattern synonyms to allow case'ing directly on the inner Either values. type (:+:) = Either instance a :<: (a :+: b) where inj = Left pick = either Just (const Nothing) instance b :<: (a :+: b) where inj = Right pick = either (const Nothing) Just instance {-# OVERLAPPABLE #-} a :<: c => a :<: (c :+: b) where inj = Left . inj pick = either pick (const Nothing) pattern Member :: x :<: c => ...