packages feed

brassica-0.0.3: src/Brassica/SoundChange/Types.hs

{-# LANGUAGE ConstraintKinds       #-}
{-# LANGUAGE DataKinds             #-}
{-# LANGUAGE DeriveAnyClass        #-}
{-# LANGUAGE DeriveGeneric         #-}
{-# LANGUAGE DerivingVia           #-}
{-# LANGUAGE FlexibleContexts      #-}
{-# LANGUAGE FlexibleInstances     #-}
{-# LANGUAGE GADTs                 #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE PolyKinds             #-}
{-# LANGUAGE RankNTypes            #-}
{-# LANGUAGE StandaloneDeriving    #-}
{-# LANGUAGE TypeFamilies          #-}
{-# LANGUAGE TypeOperators         #-}
{-# LANGUAGE UndecidableInstances  #-}

module Brassica.SoundChange.Types
       (
       -- * Words and graphemes
         Grapheme
       , PWord
       -- * Lexemes
       , Lexeme(..)
       , CategoryElement(..)
       , LexemeType(..)
       -- * Rules
       , Rule(..)
       , Environment
       , Direction(..)
       , Flags(..)
       , defFlags
       -- * Categories and statements
       , CategoriesDecl(..)
       , Statement(..)
       , plaintext'
       , SoundChanges
       -- * Utility
       , OneOf
       ) where

import Control.DeepSeq (NFData(..))
import Data.Kind (Constraint)
import GHC.Generics (Generic)
import GHC.TypeLits

-- | The constraint @OneOf a x y@ is satisfied if @a ~ x@ or @a ~ y@.
--
-- (Note: the strange @() ~ Bool@ constraint is just a simple
-- unsatisfiable constraint, so as to not give ‘non-exhaustive pattern
-- match’ errors everywhere.)
type family OneOf a x y :: Constraint where
    OneOf a a y = ()
    OneOf a x a = ()
    OneOf a b c =
        ( () ~ Bool
        , TypeError ('Text "Couldn't match type "
                     ':<>: 'ShowType a
                     ':<>: 'Text " with "
                     ':<>: 'ShowType b
                     ':<>: 'Text " or "
                     ':<>: 'ShowType c))

-- | The type of graphemes, or more accurately multigraphs: for
-- instance, @"a", "ch", "c̓" :: t'Grapheme'@.
type Grapheme = [Char]

-- | A word (or a subsequence of one) can be viewed as a list of
-- @Grapheme@s: e.g. Portuguese "filha" becomes
-- @["f", "i", "lh", "a"] :: 'PWord'@.
--
-- (The name 'PWord' is from ‘phonological word’, these being what a
-- SCA typically manipulates; this name was chosen to avoid a clash
-- with @Prelude.'Prelude.Word'@.)
type PWord = [Grapheme]

-- | The part of a 'Rule' in which a 'Lexeme' may occur: either the
-- target, the replacement or the environment.
data LexemeType = Target | Replacement | Env

-- | A 'Lexeme' is the smallest part of a sound change. Both matches
-- and replacements are made up of 'Lexeme's: the phantom type
-- variable specifies where each different variety of 'Lexeme' may
-- occur.
data Lexeme (a :: LexemeType) where
    -- | In Brassica sound-change syntax, one or more letters without intervening whitespace
    Grapheme :: Grapheme -> Lexeme a
    -- | In Brassica sound-change syntax, delimited by square brackets
    Category :: [CategoryElement a] -> Lexeme a
    -- | In Brassica sound-change syntax, specified as @#@
    Boundary :: Lexeme 'Env
    -- | In Brassica sound-change syntax, delimited by parentheses
    Optional :: [Lexeme a] -> Lexeme a
    -- | In Brassica sound-change syntax, specified as @\@
    Metathesis :: Lexeme 'Replacement
    -- | In Brassica sound-change syntax, specified as @>@
    Geminate :: Lexeme a
    -- | In Brassica sound-change syntax, specified as @^@ before another 'Lexeme'
    Wildcard :: OneOf a 'Target 'Env => Lexeme a -> Lexeme a
    -- | In Brassica sound-change syntax, specified as @*@ after another 'Lexeme'
    Kleene   :: OneOf a 'Target 'Env => Lexeme a -> Lexeme a
    -- | In Brassica sound-change syntax, specified as @~@
    Discard  :: Lexeme 'Replacement

deriving instance Show (Lexeme a)

instance NFData (Lexeme a) where
    rnf (Grapheme g) = rnf g
    rnf (Category cs) = rnf cs
    rnf Boundary = ()
    rnf (Optional ls) = rnf ls
    rnf Metathesis = ()
    rnf Geminate = ()
    rnf (Wildcard l) = rnf l
    rnf (Kleene l) = rnf l
    rnf Discard = ()

-- | The elements allowed in a 'Category': currently, only
-- t'Grapheme's and word boundaries.
data CategoryElement (a :: LexemeType) where
    GraphemeEl :: Grapheme -> CategoryElement a
    BoundaryEl :: CategoryElement 'Env

deriving instance Show (CategoryElement a)
deriving instance Eq (CategoryElement a)
deriving instance Ord (CategoryElement a)

instance NFData (CategoryElement a) where
    rnf (GraphemeEl a) = rnf a
    rnf BoundaryEl = ()

-- | An 'Environment' is a tuple of @(before, after)@ components,
-- corresponding to a ‘/ before _ after’ component of a sound change.
--
-- Note that an empty environment is just @([], [])@.
type Environment = ([Lexeme 'Env], [Lexeme 'Env])

-- | Specifies application direction of rule — either left-to-right or right-to-left.
data Direction = LTR | RTL
    deriving (Eq, Show, Generic, NFData)

-- | Flags which can be enabled, disabled or altered on a 'Rule' to
-- change how it is applied.
data Flags = Flags
  { highlightChanges :: Bool
  , applyDirection   :: Direction
  , applyOnceOnly    :: Bool
  , sporadic         :: Bool
  } deriving (Show, Generic, NFData)

-- | A default selection of flags which are appropriate for most
-- rules:
--
-- @
-- 'defFlags' = 'Flags'
--     { 'highlightChanges' = 'True'
--     , 'applyDirection' = 'LTR'
--     , 'applyOnceOnly' = 'False'
--     , 'sporadic' = 'False'
--     }
-- @
--
-- That is: highlight changes, apply from left to right, apply
-- repeatedly, and don’t apply sporadically.
defFlags :: Flags
defFlags = Flags
    { highlightChanges = True
    , applyDirection = LTR
    , applyOnceOnly = False
    , sporadic = False
    }

-- | A single sound change rule: in Brassica sound-change syntax with all elements specified,
-- @-flags target / replacement \/ environment \/ exception@.
-- (And usually the 'plaintext' of the rule will contain a 'String' resembling that pattern.)
data Rule = Rule
  { target      :: [Lexeme 'Target]
  , replacement :: [Lexeme 'Replacement]
  , environment :: Environment
  , exception   :: Maybe Environment
  , flags       :: Flags
  , plaintext   :: String
  } deriving (Show, Generic, NFData)

-- | Corresponds to a category declaration in a set of sound
-- changes. Category declarations are mostly desugared away by the
-- parser, but for rule application we still need to be able to filter
-- out all unknown t'Grapheme's; thus, a 'CategoriesDecl' lists the
-- t'Grapheme's which are available at a given point.
newtype CategoriesDecl = CategoriesDecl { graphemes :: [Grapheme] }
  deriving (Show, Generic, NFData)

-- | A 'Statement' can be either a single sound change rule, or a
-- category declaration.
data Statement = RuleS Rule | CategoriesDeclS CategoriesDecl
    deriving (Show, Generic, NFData)

-- | A simple wrapper around 'plaintext' for 'Statement's. Returns
-- @"categories … end"@ for all 'CategoriesDecl' inputs.
plaintext' :: Statement -> String
plaintext' (RuleS r) = plaintext r
plaintext' (CategoriesDeclS _) = "categories … end"

-- | A set of 'SoundChanges' is simply a list of 'Statement's.
type SoundChanges = [Statement]