brassica-1.0.0: 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 QuantifiedConstraints #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE UndecidableInstances #-}
-- |
-- Module : Brassica.SoundChange.Types
-- Copyright : See LICENSE file
-- License : BSD3
-- Maintainer : Brad Neimann
--
-- This module contains the types used to represent sound changes and
-- words in Brassica. In brief:
--
-- * A set of 'SoundChanges' is composed of a list of elements
--
-- * Their most important elements are sound change 'Rule's
--
-- * Sound changes are composed of 'Lexeme's denoting parts of the
-- input and output words
--
-- * Each word is a sequence of t'Grapheme's
--
-- For more details on the syntax and semantics of sound changes,
-- refer to the [reference guide](https://github.com/bradrn/brassica/blob/v1.0.0/docs/Reference.md).
module Brassica.SoundChange.Types
(
-- * Words and graphemes
Grapheme
, PWord
, addBoundaries
, removeBoundaries
, concatWithBoundary
-- * Lexemes
, Lexeme(..)
, LexemeType(..)
, generalise
-- * Categories
, mapCategory
, mapCategoryA
, CategoryElement
, CategorySpec(..)
, CategoryModification(..)
, Expanded(..)
, generaliseExpanded
-- * Rules
, Rule(..)
, Environment
, Direction(..)
, Sporadicity(..)
, Flags(..)
, defFlags
-- * Statements
, Filter(..)
, Statement(..)
, plaintext'
, SoundChanges
-- * Directives
, Directive(..)
, CategoryDefinition(..)
, FeatureSpec(..)
, GraphemeList(..)
) where
import Control.DeepSeq (NFData(..), deepseq)
import GHC.Generics (Generic)
import GHC.OldList (dropWhileEnd)
-- | The type of graphemes within a word. @"#"@ is taken to denote a
-- word boundary (whch is universally treated as a normal grapheme in
-- sound changes.)
type Grapheme = [Char]
-- | Brassica views a word, or a subsequence of one, as a list of
-- @Grapheme@s. For instance, Portuguese "filha" becomes
-- @["f", "i", "lh", "a"]@ when tokenised correctly.
--
-- (The name 'PWord' is from ‘phonological word’, these being what
-- sound changes typically manipulate. The name was chosen to avoid a
-- clash with @t'Word'@ from @base@.)
type PWord = [Grapheme]
-- | Add word boundaries (@"#"@) at the beginning and end of a 'PWord'.
addBoundaries :: PWord -> PWord
addBoundaries w = "#" : w ++ ["#"]
-- | Remove word boundaries (@"#"@) from the beginning and end of a 'PWord'.
removeBoundaries :: PWord -> PWord
removeBoundaries = dropWhile (=="#") . dropWhileEnd (=="#")
-- | Render a 'PWord' as a 'String': does 'removeBoundaries' then 'concat'.
concatWithBoundary :: PWord -> String
concatWithBoundary = concat . removeBoundaries
-- | The part of a 'Rule' in which a 'Lexeme' may occur.
data LexemeType
= Matched -- ^ In the target, environment or exception (‘matching’ position)
| Replacement -- ^ In the replacement only
| AnyPart -- ^ Not restricted to any one part
-- | Each part of a sound change is made up of a sequence of
-- 'Lexeme's. Each 'Lexeme' denotes part of an input or output word.
--
-- The first type variable @category@ is the type used to represent
-- categories within the sound change. This will usually be
-- 'CategorySpec' after parsing, or 'Expanded' after expansion.
--
-- The second type variable is phantom and represents the part of the
-- rule in which the lexeme is placed. Various lexemes are restricted
-- to 'Matched' or 'Replacement' positions respectively.
--
-- For details on the syntax and semantics of each kind of lexeme,
-- refer to the [reference guide](https://github.com/bradrn/brassica/blob/v1.0.0/docs/Reference.md).
data Lexeme category (a :: LexemeType) where
Grapheme :: Grapheme -> Lexeme category a
Category :: category a -> Lexeme category a
-- | Written @%category@, matching-only
GreedyCategory :: category 'Matched -> Lexeme category 'Matched
-- | Written @(lexemes)@
Optional :: [Lexeme category a] -> Lexeme category a
-- | Written @%(lexemes)@, matching-only
GreedyOptional :: [Lexeme category 'Matched] -> Lexeme category 'Matched
-- | Written @\\@, replacement-only
Metathesis :: Lexeme category 'Replacement
-- | Written @>@
Geminate :: Lexeme category a
-- | Written @^lexeme@
Wildcard :: Lexeme category a -> Lexeme category a
-- | Written @lexeme*@
Kleene :: Lexeme category a -> Lexeme category a
-- | Written @~@, replacement-only
Discard :: Lexeme category 'Replacement
-- | Written @\@n category@ or @\@#id category@
Backreference :: Either String Int -> category a -> Lexeme category a
-- | Written @\@? category@
Multiple :: category 'Replacement -> Lexeme category 'Replacement
-- | Written @lexeme$Name@ or variations (see reference guide)
Feature
:: Bool -- ^ 'True' iff the feature is negated
-> String -- ^ Feature name
-> Maybe String -- ^ Identifier if backreferenced, else 'Nothing'
-> [[Grapheme]] -- ^ List of correspondence sets
-> Lexeme category a
-> Lexeme category a
-- | Not directly available in Brassica syntax, inserted in expansion
Autosegment
:: Grapheme -- ^ Feature name
-> [[(Grapheme, Bool)]] -- ^ List of correspondence sets, with exclusion states
-> [Grapheme] -- ^ Graphemes to be matched by this 'Autosegment'
-> Lexeme category a
-- | Map a function over any categories in the given 'Lexeme'.
mapCategory :: (forall x. c x -> c' x) -> Lexeme c a -> Lexeme c' a
mapCategory _ (Grapheme g) = Grapheme g
mapCategory f (Category c) = Category (f c)
mapCategory f (GreedyCategory c) = GreedyCategory (f c)
mapCategory f (Optional ls) = Optional (mapCategory f <$> ls)
mapCategory f (GreedyOptional ls) = GreedyOptional (mapCategory f <$> ls)
mapCategory _ Metathesis = Metathesis
mapCategory _ Geminate = Geminate
mapCategory f (Wildcard l) = Wildcard (mapCategory f l)
mapCategory f (Kleene l) = Kleene (mapCategory f l)
mapCategory _ Discard = Discard
mapCategory f (Backreference i c) = Backreference i (f c)
mapCategory f (Multiple c) = Multiple (f c)
mapCategory f (Feature r n i kvs l) = Feature r n i kvs $ mapCategory f l
mapCategory _ (Autosegment n kvs gs) = Autosegment n kvs gs
-- | Like 'mapCategory', with an 'Applicative' effect.
mapCategoryA
:: Applicative t
=> (forall x. c x -> t (c' x))
-> Lexeme c a
-> t (Lexeme c' a)
mapCategoryA _ (Grapheme g) = pure $ Grapheme g
mapCategoryA f (Category c) = Category <$> f c
mapCategoryA f (GreedyCategory c) = GreedyCategory <$> f c
mapCategoryA f (Optional ls) = Optional <$> traverse (mapCategoryA f) ls
mapCategoryA f (GreedyOptional ls) = GreedyOptional <$> traverse (mapCategoryA f) ls
mapCategoryA _ Metathesis = pure Metathesis
mapCategoryA _ Geminate = pure Geminate
mapCategoryA f (Wildcard l) = Wildcard <$> mapCategoryA f l
mapCategoryA f (Kleene l) = Kleene <$> mapCategoryA f l
mapCategoryA _ Discard = pure Discard
mapCategoryA f (Backreference i c) = Backreference i <$> f c
mapCategoryA f (Multiple c) = Multiple <$> f c
mapCategoryA f (Feature r n i kvs l) = Feature r n i kvs <$> mapCategoryA f l
mapCategoryA _ (Autosegment n kvs gs) = pure $ Autosegment n kvs gs
-- | The type of a category after expansion: a simple list of
-- 'CategoryElement's.
newtype Expanded a = FromElements { elements :: [CategoryElement Expanded a] }
deriving (Eq, Ord, Show, Generic, NFData)
instance Semigroup (Expanded a) where
(FromElements es) <> (FromElements es') = FromElements (es <> es')
instance Monoid (Expanded a) where
mempty = FromElements []
-- | Generalise a @'Lexeme' c ''AnyPart'@ so it can be used in any
-- specific part of a sound change, given a way to similarly
-- generalise any categories it contains.
generalise :: (c 'AnyPart -> c a) -> Lexeme c 'AnyPart -> Lexeme c a
generalise _ (Grapheme g) = Grapheme g
generalise f (Category es) = Category $ f es
generalise f (Optional ls) = Optional $ generalise f <$> ls
generalise _ Geminate = Geminate
generalise f (Backreference i es) = Backreference i $ f es
generalise f (Wildcard l) = Wildcard $ generalise f l
generalise f (Kleene l) = Kleene $ generalise f l
generalise f (Feature r n i kvs l) = Feature r n i kvs $ generalise f l
generalise _ (Autosegment n kvs gs) = Autosegment n kvs gs
-- | Generalise an 'Expanded' category to be used in any part of a
-- sound change, similarly to 'generalise'.
generaliseExpanded :: Expanded 'AnyPart -> Expanded a
generaliseExpanded = FromElements . (fmap.fmap) (generalise generaliseExpanded) . elements
deriving instance (forall x. Show (c x)) => Show (Lexeme c a)
deriving instance (forall x. Eq (c x)) => Eq (Lexeme c a)
deriving instance (forall x. Ord (c x)) => Ord (Lexeme c a)
instance (forall x. NFData (c x)) => NFData (Lexeme c a) where
rnf (Grapheme g) = rnf g
rnf (Category cs) = rnf cs
rnf (GreedyCategory cs) = rnf cs
rnf (Optional ls) = rnf ls
rnf (GreedyOptional ls) = rnf ls
rnf Metathesis = ()
rnf Geminate = ()
rnf (Wildcard l) = rnf l
rnf (Kleene l) = rnf l
rnf Discard = ()
rnf (Backreference i l) = i `deepseq` rnf l
rnf (Multiple l) = rnf l
rnf (Feature r n i kvs l) = r `deepseq` l `deepseq` n `deepseq` i `deepseq` rnf kvs
rnf (Autosegment n kvs gs) = n `deepseq` kvs `deepseq` rnf gs
-- | An 'Environment' is a tuple of @(before, after)@ components,
-- corresponding to an environment or exception in a sound change:
-- @before _ after@.
--
-- (An empty environment is just @([], [])@.)
type Environment c = ([Lexeme c 'Matched], [Lexeme c 'Matched])
-- | Specifies application direction of rule: either left-to-right or right-to-left.
data Direction = LTR | RTL
deriving (Eq, Show, Generic, NFData)
-- | Specifies how regularly a rule should be applied. A sporadic
-- rule will produce two or more results, preserving the input as one
-- of the outputs.
data Sporadicity
= ApplyAlways
-- ^ Always apply the rule
| PerWord
-- ^ Apply sporadically, either to the whole word or to none of the word
| PerApplication
-- ^ Apply sporadically, at each application site
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 -- ^ Whether results from this sound change can be highlighted in a GUI
, applyDirection :: Direction -- ^ Direction in which to apply the rule
, applyOnceOnly :: Bool -- ^ Whether to apply the rule only once to a word
, sporadic :: Sporadicity -- ^ Whether the rule should be applied sporadically, and if so, how
, nonOverlappingTarget :: Bool -- ^ Whether the rule should apply non-iteratively (avoiding environments which overlap with targets)
} 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'
-- , 'nonOverlappingTarget' = 'False'
-- }
-- @
--
-- That is: apply repeatedly and iteratively from left to right,
-- non-sporadically, with the results available for highlighting.
defFlags :: Flags
defFlags = Flags
{ highlightChanges = True
, applyDirection = LTR
, applyOnceOnly = False
, sporadic = ApplyAlways
, nonOverlappingTarget = False
}
-- | A single sound change rule.
--
-- In Brassica sound-change syntax with all elements specified, this would be
-- @-flags target / replacement \/ environment1 \/ environment2 \/ … \/ exception@.
data Rule c = Rule
{ target :: [Lexeme c 'Matched]
, replacement :: [Lexeme c 'Replacement]
, environment :: [Environment c]
, exception :: Maybe (Environment c)
, flags :: Flags
, plaintext :: String -- ^ Rule text before parsing (displayed e.g. for debugging purposes)
} deriving (Generic)
deriving instance (forall a. Show (c a)) => Show (Rule c)
deriving instance (forall a. NFData (c a)) => NFData (Rule c)
-- | A filter, constraining the output to not match the given elements.
-- (The 'String' is the plaintext, as with 'Rule'.)
data Filter c = Filter String [Lexeme c 'Matched]
deriving (Generic)
deriving instance (forall a. Show (c a)) => Show (Filter c)
deriving instance (forall a. NFData (c a)) => NFData (Filter c)
-- | A 'Statement' within a sound change file can be a single sound
-- change rule, a filter, an instruction to report intermediate
-- results, or some other declaration.
--
-- The declaration type depends on the current sound change
-- phase. Usually it will be 'Directive' after parsing, or
-- 'GraphemeList' after expansion.
data Statement c decl
= RuleS (Rule c) -- ^ Sound change rule
| FilterS (Filter c) -- ^ Filter
| ReportS -- ^ Report intermediate result
| DeclS decl -- ^ Declaration (phase-dependent)
deriving (Generic)
deriving instance (forall a. Show (c a), Show decl) => Show (Statement c decl)
deriving instance (forall a. NFData (c a), NFData decl) => NFData (Statement c decl)
-- | A simple wrapper around 'plaintext' for 'Statement's. Returns
-- @"\<declaration\>"@ for all 'DeclS' inputs.
plaintext' :: Statement c decl -> String
plaintext' (RuleS r) = plaintext r
plaintext' (FilterS (Filter p _)) = p
plaintext' ReportS = "intermediate result"
plaintext' (DeclS _) = "<declaration>"
-- | A set of 'SoundChanges' is simply a list of 'Statement's.
type SoundChanges c decl = [Statement c decl]
-- | The individual operations used to construct a category in
-- Brassica sound-change syntax.
data CategoryModification
= Union -- ^ Written @[Category1 &Category2]@ or @[Category1 Category2]@
| Intersect -- ^ Written @[Category1 +Category2]@
| Subtract -- ^ Written @[Category1 -Category2]@
deriving (Show, Eq, Ord, Generic, NFData)
-- | A single element of a category: a sequence of 'Lexeme's. (Single
-- v'Grapheme's receive some special treatment, e.g. they can be
-- written without surrounding braces in Brassica syntax.)
type CategoryElement category a = [Lexeme category a]
-- | The specification of a category in Brassica sound-change
-- syntax. Usually this will be as a 'CategorySpec': a list of
-- 'CategoryElement's, each of which modifies the previous definition
-- using the given 'CategoryModification' method.
--
-- In some positions (e.g. after a 'Backreference') a category must be
-- provided, but that category can be predefined, to be inlined during
-- expansion. In such positions, the given category name is stored as
-- a 'MustInline' category. (In other positions predefined categories
-- are indistinguishable from normal v'Grapheme's, and represented as
-- such.)
data CategorySpec a
= CategorySpec [(CategoryModification, CategoryElement CategorySpec a)]
| MustInline String -- ^ A single grapheme assumed to have been specified earlier as a category
deriving (Show, Eq, Ord, Generic, NFData)
-- | The specification of a suprasegmental feature in Brassica
-- sound-change syntax.
--
-- Deprecated since 1.0.0.
data FeatureSpec = FeatureSpec
{ featureBaseName :: Maybe String
, featureBaseValues :: CategorySpec 'AnyPart
, featureDerived :: [(String, CategorySpec 'AnyPart)]
}
deriving (Show, Eq, Ord, Generic, NFData)
-- | A single definition within a category definition block.
data CategoryDefinition
= DefineCategory String (CategorySpec 'AnyPart)
-- ^ Defines a category with the given name and value
| DefineFeature FeatureSpec
-- ^ Defines a feature as a set of categories
| DefineAuto String
-- ^ Defines a category as autosegmental
deriving (Show, Eq, Ord, Generic, NFData)
-- | A directive used in Brassica sound-change syntax: anything which
-- occurs in a sound change file with the primary purpose of defining
-- something for later use.
data Directive
= Categories -- ^ Category definition block
Bool -- ^ Whether category was introduced with @new@
Bool -- ^ Whether category was introduced with @noreplace@
[CategoryDefinition]
| ExtraGraphemes [String]
-- ^ Extra graphemes declaration: @extra …@
deriving (Show, Eq, Ord, Generic, NFData)
-- | A list of graphemes, replacing v'Categories' in expanded sound
-- changes. These are used in tokenisation to determine which
-- multigraphs are used, and in rule application to filter unwanted
-- graphemes. The first 'Bool' indicates whether filtration should
-- occur for any particular categories block.
data GraphemeList = GraphemeList Bool [Grapheme]
deriving (Eq, Show, Ord, Generic, NFData)