Opened 3 years ago

Last modified 9 months ago

#12001 new feature request

RFC: Add pattern synonyms to base

Reported by: Iceland_jack Owned by:
Priority: normal Milestone:
Component: libraries/base Version: 7.10.3
Keywords: Cc:
Operating System: Unknown/Multiple Architecture: Unknown/Multiple
Type of failure: None/Unknown Test Case:
Blocked By: Blocking:
Related Tickets: Differential Rev(s):
Wiki Page:

Description (last modified by Iceland_jack)

Since we have pattern synonyms it's worth considering if some belong in base:

Data.Complex.Lens contains patterns that could be defined in base, here are some more suggestions:

Data.Array

pattern ArrayIx :: Ix i => (i, i) -> [(i, e)] -> Array i e
pattern ArrayIx low'high xs <- ((\arr -> (bounds arr, assocs arr)) -> (low'high, xs)) 
  where ArrayIx low'high xs = array low'high xs

Data.Bits

pattern ZeroBits :: (Eq a, Bits a) => a                                         
pattern ZeroBits <- ((== zeroBits) -> True) 
  where ZeroBits = zeroBits                                                        

pattern BitSize :: Bits a => Int -> a                                            
pattern BitSize n <- (bitSizeMaybe -> Just n)                                        

pattern Signed :: Bits a => a                                                   
pattern Signed <- (isSigned -> True)                                               

pattern Unsigned :: Bits a => a                                                 
pattern Unsigned <- (isSigned -> False)                                              

pattern PopCount :: Bits a => Int -> a                                           
pattern PopCount n <- (popCount -> n)                                                

Data.Char

pattern ControlChar :: Char                                                         
pattern ControlChar <- (isControl -> True)                                           

pattern SpaceChar :: Char                                                           
pattern SpaceChar <- (isSpace -> True)                                               

Data.Complex

import Data.Complex (Complex, conjugate, polar, mkPolar)
import qualified Data.Complex as C

pattern Conjugate :: Num a => Complex a -> Complex a
pattern Conjugate a <- (conjugate -> a) 
  where Conjugate a = conjugate a

pattern Polar :: RealFloat a => a -> a -> Complex a
pattern Polar m theta <- (polar -> (m, theta))
  where Polar m theta = mkPolar m theta

-- See https://github.com/ekmett/lens/issues/653
pattern Real :: Num a => a -> Complex a
pattern Real r <- r C.:+ _
  where Real r =  r C.:+ 0

pattern Imaginary :: Num a => a -> Complex a
pattern Imaginary i <- _ C.:+ i
  where Imaginary i =  0 C.:+ i

pattern (:+) :: a -> a -> Complex a
pattern (:+) { realPart, imagPart } = realPart C.:+ imagPart 

GHC.Float

pattern NegativeZero :: RealFloat a => a
pattern NegativeZero <- (isNegativeZero -> True)
  where NegativeZero = -0

pattern Denormalized :: RealFloat a => a
pattern Denormalized <- (isDenormalized -> True)

pattern NaN :: RealFloat a => a
pattern NaN <- (isNaN -> True)
  where NaN = 0 / 0

-- How ever negative infinity is handled
pattern Infinity :: RealFloat a => a
pattern Infinity <- (isInfinite -> True)
  where Infinity = 1 / 0

Foreign.Ptr

pattern NullPtr :: Ptr a
pattern NullPtr <- ((==) nullPtr -> True)
  where NullPtr = nullPtr

Used here

Change History (16)

comment:1 Changed 3 years ago by Iceland_jack

pattern II :: Int -> Int#
pattern II i      <- (I# -> i) 
  where II (I# i) = i

pattern FF :: Float -> Float#
pattern FF f      <- (F# -> f) 
  where FF (F# f) = f

also if GHC allowed unlifted types in pattern synonyms, should that be a ticket?

tImB.hs:6:22-25: error: …
    • Expecting a lifted type, but ‘Int#’ is unlifted
    • In the type ‘Int#’
tImB.hs:10:22-27: error: …
    • Expecting a lifted type, but ‘Float#’ is unlifted
    • In the type ‘Float#’
Compilation failed.

comment:2 Changed 3 years ago by Iceland_jack

Some random ideas,

Data.Functor.Const

pattern K :: forall a (b :: k). a -> Const a b
pattern K a = Const a

Data.Functor.Constant

pattern K' :: forall a (b :: k). a -> Constant a b
pattern K' a = Constant a

GHC.Generics

isAssociative :: Associativity -> Bool
isAssociative  LeftAssociative = True
isAssociative RightAssociative = True
isAssociative   NotAssociative = False

pattern Associative :: Associative
pattern Associative <- (isAssociative -> True)

Data.Ratio

pattern Denominator :: a -> Ratio a
pattern Denominator a <- (denominator -> a)

pattern Numerator :: a -> Ratio a
pattern Numerator a <- (numerator -> a)

pattern (:%) :: Integral a => a -> a -> Ratio a
pattern num :% den <- ((\r -> (numerator r, denominator r)) -> (num, den))
  where num :% den =  num % den

System.Exit

exitCode :: ExitCode -> Int
exitCode = \case
  ExitSuccess   -> 0
  ExitFailure n -> n

pattern ExitCode :: Int -> ExitCode
pattern ExitCode n <- (exitCode -> n)
  where ExitCode 0 = ExitSuccess
        ExitCode n = ExitFailure n

Data.Maybe

pattern Some :: a -> Maybe a
pattern Some a = Just a

pattern None :: Maybe a
pattern None = Nothing

Data.Functor.Identity

pattern I :: a -> Identity
pattern I a = Identity a

Data.Ord

pattern Less :: Ordering
pattern Less = LT

pattern Equal :: Ordering
pattern Equal = EQ

pattern Greater :: Ordering
pattern Greater = GT

Data.Foldable

pattern Null :: Foldable f => f a
pattern Null <- (null -> True)

Data.Bool

pattern T :: Bool
pattern T = True

pattern F :: Bool
pattern F = False

It's fine if these don't get accepted, just throwing them into the universe

Last edited 3 years ago by Iceland_jack (previous) (diff)

comment:3 Changed 3 years ago by nomeata

I am very sceptical. Pattern synonyms are still relatively new, and I would say that best practices around them have not evolved yet (e.g.: should they be used for non-injective convenience patterns like Popcount at all? Should they be used for anything else but compatible or alternative views on data types?)

Also, they clutter the namespace.

Therefore, I would prefer if less central libraries would be first to take up pattern synonym and explore usage patterns, until it becomes apparent with which intensity pattern synonym make most sense.

More concretely, of your list above, I’d give a +1 only to Polar. This is a genuine, self-descriptive constructor-like view on a data type. A bit fishy around 0, but that’s inherent in the general concept and not an issue with the constructor.

comment:4 Changed 3 years ago by ekmett

I'm pretty much in the same camp as Joachim: The only one of these that I think really passes muster as a pattern that models a constructor is Polar.

It doesn't destroy information when you pattern match with it and then reconstruct. (It does, however, destroy the phase information if the magnitude is 0 if you construct then deconstruct).

The rest seem all better managed as view patterns, using existing combinators so that their lossy nature is much more clear.

It is worthy of discussion to explore whether we're ready to start incorporating patterns into the bulk of base, but I personally think I'd like to see them endure a couple of releases without the sorts of major overhauls they have going on with how to put signatures on them, etc. before they started taking a more prominent role in a place where they'd be as hard to dislodge as base.

comment:5 Changed 3 years ago by Iceland_jack

Great responses, there are obviously no best practices yet for this extension.

As the dust settles, will we only accept constructor-like patterns? Are non-constructor-like patterns wholly undesirable or are there examples where they are worth it? I personally use all of these to good effect, let's wait until we understand them better.

Let's identify and categorise some of these examples:

Literal synonyms

Bidirectional patterns, nothing fancy

pattern I a  = Identity a
pattern K a  = Const a
pattern Less = LT

‘Getter’ patterns

Unidirectional patterns, these are essentially ‘Getter’s

pattern Real      r <- r :+ _
pattern Imaginary i <- _ :+ i

Constructor-Like Patterns

(Implicitly) bidirectional patterns that don't lose information

pattern Real, Imaginary :: (Num a, Eq a) => a -> Complex 
pattern Real      r = r :+ 0
pattern Imaginary i = 0 :+ i

In this case this is the identity function on complex numbers:

f :: (Num a, Eq a) => Complex a -> Complex a
f (Real r) = Real r
f complex  = complex

@ekmett argues for this property here. It will fail to match any complex number with a non-zero imaginary part: The way I see it, this presents a bona fide design decision that I would like to see discussed.

Non-Constructor-Like Patterns

Explicitly bidirectional patterns that do lose information

pattern Real, Imaginary :: Num a => a -> Complex a
pattern Real r <- r :+ _ 
  where Real r =  r :+ 0

pattern Imaginary i <- _ :+ i 
  where Imaginary i =  0 :+ i

We lose the constructor property, but matching never fails so it's closer to the lens _realPart.


We can keep this ticket open until the extension and understanding matures, same as #11349 where Data.Proxyless was created in response. It has been said of me that I embrace pattern synonyms to “almost an absurd level” so someone needs to show restraint :--) tickets such as #11977 show that this extension still isn't even fully understood and I look forward to seeing how it evolves

Last edited 3 years ago by Iceland_jack (previous) (diff)

comment:6 Changed 3 years ago by Iceland_jack

Since Polar was the pick of the litter, it's worth mentioning that with record syntax

pattern Polar :: RealFloat a => a -> a -> Complex a
pattern Polar {m, theta} <- (polar -> (m, theta))
  where Polar m theta = mkPolar m theta

we can write

ghci> (3 :+ 1) { m = 2 }
1.8973665961010275 :+ 0.6324555320336759
ghci>
ghci> set (_polar._1) 2 (3 :+ 1)
1.8973665961010275 :+ 0.6324555320336759
Last edited 3 years ago by Iceland_jack (previous) (diff)

comment:7 Changed 3 years ago by Iceland_jack

Jotting down ideas.

GHC.Generics

pattern From :: Generic a => (Rep a) x -> a
pattern From rep <- (from -> rep)
  where From rep = to rep

pattern From1 :: Generic1 f => (Rep1 f) a -> f a
pattern From1 rep <- (from1 -> rep)
  where From1 rep = to1 rep

or corresponding To, To1 patterns.

Text.Read

pattern Read :: Read a => a -> String
pattern Read a <- (readMaybe -> Just a)

with the caveat that they parse different types,

foo :: String -> String
foo (Read 42) = "answer"
foo (Read n)  = "some other number " ++ show n
foo _         = "can't parse"
ghci> foo "()"
"some other number ()"

If the types are made explicit with #11350

foo :: String -> String
foo (Read @Integer 42) = "answer"
foo (Read @()      n)  = "some other number " ++ show n
foo _                  = "can't parse"

it is a common action and pattern (search for pattern and readMaybe -> Just), and is often use to pattern match on numbers. Artificially restricting the type avoids it matching ()

pattern ReadNumber :: (Num a, Read a) => a -> String

This is cool

ghci> [ n | ReadNumber n <- words "from 2010 ... 2016 they were 4" ]
[2010,2016,4]
Last edited 3 years ago by Iceland_jack (previous) (diff)

comment:8 Changed 3 years ago by Iceland_jack

Not sure if makes sense

Control.DeepSeq

https://www.reddit.com/r/haskell_jp/comments/74f2tr/pepeiborrastricttypes_type_and_value_level/

pattern Force :: NFData a => a -> a
pattern Force a <- !(force -> a)

Debug.Trace

pattern Dbg :: a
pattern Dbg <- ((\_ -> trace "DEBUG" False) -> True)

-- ?
pattern DbgShow :: Show a => a
pattern DbgShow <- ((\a -> trace ("DEBUG: " ++ show a) False) -> True)

pattern YesDbgShow :: Show a => a -> a
pattern YesDbgShow a <- ((\a -> trace ("DEBUG: " ++ show a) a) -> a)
  where YesDbgShow (!a) = trace ("DEBUG 2: " ++ show a) a `seq` a
-- >>> foob "abc"
-- "DEBUG
-- abcabc"
foob :: String -> String
foob Dbg = ...
foob a   = a ++ a

teaser:

pattern Dbg :: forall a sym. KnownSymbol sym => a
pattern Dbg <- ((\_ -> trace (symbolVal (Proxy :: Proxy sym)) False) -> True)
Last edited 2 years ago by Iceland_jack (previous) (diff)

comment:9 Changed 3 years ago by Iceland_jack

With UnboxedSums

pattern Left# :: a -> (# a | b #)
pattern Left# a = (# a | #)

pattern Right# :: b -> (# a | b #)
pattern Right# b = (# | b #)

functions that return Int# for True/False (it won't accept a pattern signature with the unlifted type)

pattern False# :: Int#
pattern False# = 0#

pattern True# :: Int#
pattern True# = 1#

TODO When we get newtypes over unlifted types add

newtype Bool# = Bool# Int#

pattern False# :: Bool#
pattern False# = Bool 0#

pattern True# :: Bool#
pattern True# = Bool 1#

(==#)  :: Int#    -> Int#    -> Bool#
(==##) :: Double# -> Double# -> Bool#
Last edited 2 years ago by Iceland_jack (previous) (diff)

comment:10 Changed 3 years ago by Iceland_jack

See #12767:

import qualified Control.Monad.Cont   as C
import           Control.Monad.Cont   hiding (runCont)
import qualified Control.Monad.Writer as W
import           Control.Monad.Writer hiding (runWriter)
import qualified Control.Monad.Reader as R
import           Control.Monad.Reader hiding (runReader)
import qualified Control.Monad.State  as S
import           Control.Monad.State  hiding (runState)

pattern Cont :: ((a -> r) -> r) -> Cont r a
pattern Cont {runCont} <- (C.runCont -> runCont)
  where Cont a          = cont a

pattern Writer :: (a, w) -> Writer w a
pattern Writer {runWriter} <- (W.runWriter -> runWriter)
  where Writer a            = WriterT (Identity a)

pattern Reader :: (r -> a) -> Reader r a
pattern Reader {runReader} <- (R.runReader -> runReader)
  where Reader a            = reader a

pattern State :: (s -> (a, s)) -> State s a
pattern State {runState} <- (S.runState -> runState)
  where State a           = state a
Last edited 3 years ago by Iceland_jack (previous) (diff)

comment:11 Changed 3 years ago by Iceland_jack

For Data.List.NonEmpty

pattern NonEmpty :: NonEmpty a -> [a]
pattern NonEmpty xs <- (nonEmpty -> Just xs)
  where NonEmpty xs = toList xs

{-# COMPLETE [], NonEmpty  #-}

using PatternSynonyms/CompleteSigs.

Use

The pattern of destructing a NonEmpty value only to reassemble it pops up a lot when using NonEmpty

f []     = ...
f (x:xs) = ... (x:|xs)

An example is

-- match_groups :: [[(PatGroup,EquationInfo)]] -> DsM (NonEmpty MatchResult)
-- match_groups []     = matchEmpty v ty
-- match_groups (g:gs) = mapM match_group (g:|gs)

match_groups :: [[(PatGroup,EquationInfo)]] -> DsM (NonEmpty MatchResult)
match_groups []            = matchEmpty v ty
match_groups (NonEmpty gs) = mapM match_group gs

Which can be abstracted into this rather useful eliminator:

-- match_groups = neElim (matchEmpty v ty) (mapM match_group)

neElim :: c -> (NonEmpty a -> c) -> [a] -> c
neElim c _ []            = c
neElim _ f (NonEmpty xs) = f xs

Use

Can be used twice in

snocView :: forall a. [a] -> Maybe ([a],a)
snocView []     = Nothing
snocView (x:xs) = go [] (x:|xs) where

  go :: [a] -> NonEmpty a -> Maybe ([a], a)
  go acc = \case
    x:|[]   -> Just (reverse acc, x)
    x:|y:ys -> go (x:acc) (y:|ys)

which becomes

snocView :: forall a. [a] -> Maybe ([a],a)
snocView []            = Nothing
snocView (NonEmpty xs) = go [] xs where

  go :: [a] -> NonEmpty a -> Maybe ([a], a)
  go acc = \case
    x :|[]          -> Just (reverse acc, x)
    x :|NonEmpty ys -> go (x:acc) ys

or

snocView :: forall a. [a] -> Maybe ([a],a)
snocView = neElim Nothing (go []) where

  go :: [a] -> NonEmpty a -> Maybe ([a], a)
  go acc (x:|xs) = neElim (Just (reverse acc, x)) (go (x:acc)) xs

using neElim.

Use

GCD

-- gcd' (x :| y:ys) = gcd x (gcd' (y:|ys))

gcd' :: GCDDomain r => NonEmpty r -> r
gcd' (x :| [])          = normalize x
gcd' (x :| [y])         = gcd x y
gcd' (x :| NonEmpty ys) = gcd x (gcd' ys)

Use

-- foldMapWithKey1 f (Node a (x:xs)) = f empty a <> foldMapWithKey1 (foldMapWithKey1 . fmap f . flip (|>)) (x:|xs)
foldMapWithKey1 f (Node a (NonEmpty xs)) = f empty a <> foldMapWithKey1 (foldMapWithKey1 . fmap f . flip (|>)) xs

-- traverseWithKey1 f (Node a (x:xs)) = Node <$> f empty a <.> traverseWithKey1 (traverseWithKey1 . fmap f . flip (|>)) (x:|xs)
traverseWithKey1 f (Node a (NonEmpty xs)) = Node <$> f empty a <.> traverseWithKey1 (traverseWithKey1 . fmap f . flip (|>)) xs

-- foldMapWithKey1 f (x:|(y:ys)) = f 0 x <> foldMapWithKey1 (f . (+1)) (y:|ys)
foldMapWithKey1 f (x:|NonEmpty xs) = f 0 x <> foldMapWithKey1 (f . (+1)) xs

-- unwrap (_ :| [])       = Nothing
-- unwrap (_ :| (a : as)) = Just (a :| as)
unwrap (_ :| [])          = Nothing
unwrap (_ :| NonEmpty as) = Just as

or

unwrap (_ :| xs) = neElim Nothing Just xs

Use

JSON

parseJSON = withArray "NonEmpty [a]" $ \v -> case toList v of
  [] -> fail "Non-empty list required"
  -- (x:xs) -> mapM parseJSON (x :| xs)
  NonEmpty xs -> mapM parseJSON xs

or

parseJSON = withArray "NonEmpty [a]" $ neElim (fail "Non-empty list required") (mapM parseJSON)

If we use neElim that eliminates a Foldable.

Use

github

-- stitch (x0:|(x1:xs)) y = x0 <| stitch (x1:|xs) y
stitch (x0:|NonEmpty xs) y = x0 <| stitch xs y
Last edited 3 years ago by Iceland_jack (previous) (diff)

comment:12 Changed 2 years ago by Iceland_jack

Description: modified (diff)

comment:13 Changed 2 years ago by Iceland_jack

Examples added to gist.

comment:14 Changed 22 months ago by Iceland_jack

Description: modified (diff)

comment:15 Changed 9 months ago by Iceland_jack

Description: modified (diff)

comment:16 Changed 9 months ago by Iceland_jack

Description: modified (diff)
Note: See TracTickets for help on using tickets.