packages feed

willow-0.1.0.0: src/Web/Willow/Common/Parser/Switch.hs

{-|
Description:    A monad combinator emulating greedy pattern matching.

Copyright:      (c) 2020 Sam May
License:        MPL-2.0
Maintainer:     ag.eitilt@gmail.com

Stability:      provisional
Portability:    portable

'A.Alternative' instances can provide a form of pattern matching if given a
fail-on-false combinator (e.g. 'Control.Monad.when'), however the exact
behaviour isn't guaranteed; an underlying 'Maybe' does provide a greedy match,
but @[]@ will match later overlapping tests even if they are intended to be
masked; compare the masking to standard, cascading pattern guards.  This module
provides a means of formalizing that behaviour into a predictable form, no
matter which 'A.Alternative' winds up being used.
-}
module Web.Willow.Common.Parser.Switch
    ( SwitchCase ( .. )
    , switch
    ) where


import qualified Control.Applicative as A

import qualified Data.Foldable as D
import qualified Data.Either as E


-- | Run a block of 'SwitchCase's, collapsing any masking cases so that only
-- the first matched test remains.  This is strictly more powerful than pattern
-- matching, as it allows interspersing non-masking tests alongside masking
-- ones; for compatibility with refactoring to single-return 'A.Alternative'
-- instances, however (i.e. 'Maybe'), it's best to order everything /as if/
-- every case could mask the ones after it.  Note that the masking only affects
-- the output; the tests themselves may still be run, so expensive computations
-- are best put elsewhere.
-- 
-- Only the first overlapping (maskable) case is selected:
-- 
-- >>> uppercase = If_   isUpper  $ return "uppercase"
-- >>> one       = When_ (== '1') $ return "single '1'"
-- >>> alpha     = If_   isAlpha  $ return "ASCII letter"       -- Matches
-- >>> catchall  = Else_          $ return "none of the above"  -- Matches
-- >>> switch [uppercase, one, alpha, catchall] 'a' :: [String]
-- ["ASCII letter"]
-- 
-- Non-masking cases don't interact with the masking calculations:
-- 
-- >>> uppercase = If_   isUpper  $ return "uppercase"
-- >>> one       = When_ (== '1') $ return "single '1'"         -- Matches
-- >>> alpha     = If_   isAlpha  $ return "ASCII letter"
-- >>> catchall  = Else_          $ return "none of the above"  -- Matches
-- >>> switch [uppercase, one, alpha, catchall] '1' :: [String]
-- ["single '1'", "none of the above"]
-- 
-- 'Maybe' always takes the earliest successful test:
-- 
-- >>> uppercase = If_   isUpper  $ return "uppercase"
-- >>> one       = When_ (== '1') $ return "single '1'"         -- Matches
-- >>> alpha     = If_   isAlpha  $ return "ASCII letter"
-- >>> catchall  = Else_          $ return "none of the above"  -- Matches
-- >>> switch [uppercase, one, alpha, catchall] '1' :: Maybe String
-- Just "single '1'"
-- 
-- 'Always' and 'Always_' function as a standard 'A.Alternative' computation:
-- 
-- >>> switch [Always a, Always b, Always_ c] tok == a tok <|> b tok <|> c
-- True
switch :: A.Alternative m => [SwitchCase test m out] -> test -> m out
switch cases test = D.asum . map (either id id) $
    -- Reduce the list to all 'Right's and only a single (the first) 'Left'.
    headRights ++ take 1 remaining ++ filter E.isRight (drop 1 remaining)
  where (headRights, remaining) = span E.isRight $ foldr switch' [] cases
        switch' (If f p) m
            | f test = Left (p test) : m
            | otherwise = m
        switch' (If_ f p) m
            | f test = Left p : m
            | otherwise = m
        switch' (Else p) m = Left (p test) : m
        switch' (Else_ p) m = Left p : m
        switch' (When f p) m
            | f test = Right (p test) : m
            | otherwise = m
        switch' (When_ f p) m
            | f test = Right p : m
            | otherwise = m
        switch' (Always p) m = Right (p test) : m
        switch' (Always_ p) m = Right p : m


-- | The building blocks for predictable pattern matches over 'A.Alternative'.
-- The constructors are distinguished along three axes (see also the examples
-- in the documentation for 'switch'):
-- 
-- * "masking" vs. "non-masking": only the first "masking" case fulfilled will
--   be returned, while /every/ "non-masking" one is returned
-- * "matching" vs. "catchall": whether the output is gated by a predicate test
--   or not
-- * "piped" vs. "static": whether the output is passed the original test token
data SwitchCase test m out
    = If (test -> Bool) (test -> m out)
        -- ^ Masking, matching, and piped
    | If_ (test -> Bool) (m out)
        -- ^ Masking, matching, and static
    | Else (test -> m out)
        -- ^ Masking, catchall, and piped
    | Else_ (m out)
        -- ^ Masking, catchall, and static
    | When (test -> Bool) (test -> m out)
        -- ^ Non-masking, matching, and piped
    | When_ (test -> Bool) (m out)
        -- ^ Non-masking, matching, and static
    | Always (test -> m out)
        -- ^ Non-masking, catchall, and piped
    | Always_ (m out)
        -- ^ Non-masking, catchall, and static