functional-programmingvariantpurescript

Usecase of Variants in Purescript/Haskell


Can someone tell me what is the use case of purescript-variants or variants in general

The documentation is very well written but I can't find any real use case scenario for it. Can someone tell how we could use Variants in real world?


Solution

  • Variants are duals of records. While records are sort of extensible ad-hoc product types (consider data T = T Int String vs. type T = { i :: Int, s :: String }), variants can be seen as extensible ad-hoc sum types - e.g. data T = A Int | B String vs. Variant (a :: Int, b :: String)

    For example, just as you can write a function that handles a partial record:

    fullName :: forall r. { first :: String, last :: String | r } -> String
    fullName r = r.first <> " " <> r.last
    
    myFullName = fullName { first: "Fyodor", last: "Soikin", weight: "Too much" }
    

    so too, you can write a function that handles a partial variant:

    weight :: forall r. Variant (kilos :: Int, vague :: String | r) -> String
    weight = 
      default "Unknown"
      # on _kilos (\n -> show n <> " kg.")
      # on _vague (\s -> "Kind of a " <> s)
    
    myWeight = weight (inj _kilos 100)  -- "100 kg."
    
    alsoMyWeight = weight (inj _vague "buttload")  -- "Kind of a buttload"
    

    But these are, of course, toy examples. For a less toy example, I would imagine something that handles alternatives, but needs to be extensible. Perhaps something like a file parser:

    data FileType a = Json | Xml
    
    basicParser :: forall a. FileType a -> String -> Maybe a
    basicParser t contents = case t of
      Json -> parseJson contents
      Xml -> parseXml contents
    

    Say I'm ok using this parser in most case, but in some cases I'd also like to be able to parse YAML. What do I do? I can't "extend" the FileType sum type post-factum, the best I can do is aggregate it in a larger type:

    data BetterFileType a = BasicType (FileType a) | Yaml
    
    betterParser :: forall a. BetterFileType a -> String -> Maybe a
    betterParser t contents = case t of
      BasicType bt -> basicParser bt contents
      Yaml -> parseYaml contents
    

    And now whenever I call the "better parser", I have to wrap the file type awkwardly:

    result = betterParser (BasicType Json) "[1,2,3]"
    

    Worse: now every consumer has to know the hierarchy of BetterFileType -> FileType, they can't just say "json", they have to know to wrap it in BasicType. Awkward.

    But if I used extensible variants for the file type, I could have flattened them nicely:

    type FileType r = (json :: String, xml :: String | r)
    
    basicParser :: forall a r. Variant (FileType r) -> Maybe a
    basicParser = onMatch { json: parseJson, xml: parseXml } $ default Nothing
    ----
    type BetterFileType r = (yaml :: String | FileType r)
    
    betterParser :: forall a r. Variant (BetterFileType r) -> Maybe a
    betterParser = onMatch { yaml: parseYaml } basicParser
    

    Now I can use the naked variant names with either basicParser or betterParser, without knowing to wrap them or not:

    r1 = betterParser $ inj _json "[1,2,3]"
    r2 = betterParser $ inj _yaml "foo: [1,2,3]"