Back to Index Page

TinyAPL part 10: Wraps, Structs, a Standard Library

#tinyapl#apl#haskell

Madeline Vergani

Strap in, this is a long one. Three important things are added to TinyAPL in this post: wraps (first-class functions and modifiers), structs (think Dyalog namespaces) and a standard library. Along these many primitives have also been implemented, including fundamental ones like Catenate or Transpose.

Just like always, you can see all the code for this post in the GitHub repository

Separators and Comments

Single newlines have been changed to not be statement separators instead; if you want to separate use a diamond or a double newline. Line comments have also been added. This is made very easy with Megaparsec's Lexer:

spaceConsumer = L.space (void $ satisfy (liftA2 (&&) isSpace (/= '\n')) <|> try (char '\n' <* notFollowedBy (char '\n'))) (L.skipLineComment [G.comment]) (L.skipBlockComment [fst G.inlineComment] [snd G.inlineComment])

Additionally, empty statements are allowed and they evaluate to the empty vector.

Repeat and Until

Repeat is a conjunction that applies a function a specified amount of times:

repeat :: MonadError Error m => (a -> m a) -> Natural -> a -> m a
repeat _ 0 x = pure x
repeat f n x = f x >>= TinyAPL.Functions.repeat f (n - 1)

repeat1 :: MonadError Error m => (Array -> m Array) -> Array -> Array -> m Array
repeat1 f t y = do
  let err = DomainError "Repeat right operand must be a natural scalar"
  n <- asScalar err t >>= asNumber err >>= asNat err
  TinyAPL.Functions.repeat f n y

repeat2 :: MonadError Error m => (Array -> Array -> m Array) -> Array -> Array -> Array -> m Array
repeat2 f t x y = do
  let err = DomainError "Repeat right operand must be a natural scalar"
  n <- asScalar err t >>= asNumber err >>= asNat err
  TinyAPL.Functions.repeat (f x) n y

Until calls the left function until the right one returns true:

until :: MonadError Error m => (a -> m a) -> (a -> a -> m Bool) -> a -> m a
until f p x = let
  go :: Monad m => (a -> m a) -> (a -> a -> m Bool) -> a -> a -> m a
  go f p prev x = do
    r <- f x
    t <- p r prev
    if t then pure r else go f p x r
  in f x >>= go f p x

until1 :: MonadError Error m => (Array -> m Array) -> (Array -> Array -> m Array) -> Array -> m Array
until1 f p y = let
  err = DomainError "Until right operand must return a boolean scalar"
  in TinyAPL.Functions.until f (\cu pr -> p cu pr >>= asScalar err >>= asBool err) y

until2 :: MonadError Error m => (Array -> Array -> m Array) -> (Array -> Array -> m Array) -> Array -> Array -> m Array
until2 f p x y = let
  err = DomainError "Until right operand must return a boolean scalar"
  in TinyAPL.Functions.until (f x) (\cu pr -> p cu pr >>= asScalar err >>= asBool err) y

Catenate

The normal APL catenate function, which promotes when rank is 1-different and reshapes scalars.

catenate :: MonadError Error m => Array -> Array -> m Array
catenate a@(Array ash acs) b@(Array bsh bcs) =
  if arrayRank a == arrayRank b then
    if (isScalar a && isScalar b) || (tailMaybe ash == tailMaybe bsh) then pure $ fromMajorCells $ majorCells a ++ majorCells b
    else throwError $ LengthError "Incompatible shapes to Catenate"
  else if isScalar a then catenate (fromJust $ arrayReshaped (1 : tailPromise bsh) acs) b
  else if isScalar b then catenate a (fromJust $ arrayReshaped (1 : tailPromise ash) bcs)
  else if arrayRank a == arrayRank b + 1 then promote b >>= (a `catenate`)
  else if arrayRank a + 1 == arrayRank b then promote a >>= (`catenate` b)
  else throwError $ RankError "Incompatible ranks to Catenate"

TAO Operations

TinyAPL currently supports two TAO comparisons (i.e. comparisons that operate on arrays as a whole and not on each element): Identical (the equivalent of =) and Not Identical (the equivalent of ). Here are the four missing ones: Precedes (the equivalent of <), Precedes or Identical (the equivalent of ), Succeeds or Identical (the equivalent of ) and Succeeds (the equivalent of >).

precedes :: MonadError Error m => Array -> Array -> m Bool
precedes x y = pure $ x < y

precedes' :: MonadError Error m => Array -> Array -> m Array
precedes' x y = scalar . boolToScalar <$> precedes x y

precedesOrIdentical :: MonadError Error m => Array -> Array -> m Bool
precedesOrIdentical x y = pure $ x <= y

precedesOrIdentical' :: MonadError Error m => Array -> Array -> m Array
precedesOrIdentical' x y = scalar . boolToScalar <$> precedesOrIdentical x y

succeedsOrIdentical :: MonadError Error m => Array -> Array -> m Bool
succeedsOrIdentical x y = pure $ x >= y

succeedsOrIdentical' :: MonadError Error m => Array -> Array -> m Array
succeedsOrIdentical' x y = scalar . boolToScalar <$> succeedsOrIdentical x y

succeeds :: MonadError Error m => Array -> Array -> m Bool
succeeds x y = pure $ x > y

succeeds' :: MonadError Error m => Array -> Array -> m Array
succeeds' x y = scalar . boolToScalar <$> succeeds x y

Two other functions that can be TAO-ified are Minimum and Maximum, which respectively become Minimal and Maximal .

minimal :: MonadError Error m => Array -> Array -> m Array
minimal x y = pure $ Prelude.min x y

maximal :: MonadError Error m => Array -> Array -> m Array
maximal x y = pure $ Prelude.max x y

Grading and Sorting

Six functions: Grade Up , Grade Down , Sort By Up , Sort By Down , Sort Up , Sort Down .

gradeUp :: MonadError Error m => Ord a => [a] -> m [Natural]
gradeUp xs = pure $ map fst $ sortOn snd $ zip [1..genericLength xs] xs

gradeUp' :: MonadError Error m => Array -> m Array
gradeUp' arr = vector . fmap (Number . fromInteger . toInteger) <$> gradeUp (majorCells arr)

gradeDown :: MonadError Error m => Ord a => [a] -> m [Natural]
gradeDown xs = pure $ map fst $ sortOn snd $ zip [1..genericLength xs] (Down <$> xs)

gradeDown' :: MonadError Error m => Array -> m Array
gradeDown' arr = vector . fmap (Number . fromInteger . toInteger) <$> gradeDown (majorCells arr)

sortByUp :: MonadError Error m => Ord b => [a] -> [b] -> m [a]
sortByUp as bs = pure $ map fst $ sortOn snd $ zip as bs

sortByUp' :: MonadError Error m => Array -> Array -> m Array
sortByUp' as bs = fromMajorCells <$> sortByUp (majorCells as) (majorCells bs)

sortByDown :: MonadError Error m => Ord b => [a] -> [b] -> m [a]
sortByDown as bs = pure $ map fst $ sortOn snd $ zip as $ Down <$> bs

sortByDown' :: MonadError Error m => Array -> Array -> m Array
sortByDown' as bs = fromMajorCells <$> sortByDown (majorCells as) (majorCells bs)

sortUp :: MonadError Error m => Ord a =>[a] -> m [a]
sortUp = pure . sort

sortUp' :: MonadError Error m => Array -> m Array
sortUp' = fmap fromMajorCells . sortUp . majorCells

sortDown :: MonadError Error m => Ord a => [a] -> m [a]
sortDown = pure . fmap getDown . sort . fmap Down

sortDown' :: MonadError Error m => Array -> m Array
sortDown' = fmap fromMajorCells . sortDown . majorCells

Transpose

My implementation of (dyad) Transpose is taken (or stolen) from Tests, Derivations, Proofs by Hui.

reorderAxes' :: MonadError Error m => Array -> Array -> m Array
reorderAxes' x y = do
  shy <- shape' y
  sx <- sortUp' x
  is <- sortByUp' shy x
  is' <- key' ((reduce' min') `atop` (pure .: flip const)) sx is
  iota <- indexGenerator' is'
  indices <- eachRight from x iota
  from indices y

reorderAxes :: MonadError Error m => [Natural] -> Array -> m Array
reorderAxes is arr = reorderAxes' (vector $ Number . fromInteger . toInteger <$> is) arr

transpose :: MonadError Error m => Array -> m Array
transpose arr = do
  r <- rank' arr >>= indexGenerator' >>= reverse'
  reorderAxes' r arr

Valences

Ah, an easy one! Valences calls the left function if called monadically and the right function if called dyadically. In fact, it's so easy it doesn't even have a definition in Functions, so here's the Primitives definition:

valences = Conjunction
  { conjRepr = [G.valences]
  , conjOnArrayArray = Nothing
  , conjOnArrayFunction = Nothing
  , conjOnFunctionArray = Nothing
  , conjOnFunctionFunction = Just $ \f g -> pure $ Function (Just $ callMonad f) (Just $ callDyad g) (makeConjRepr (show f) G.valences (show g)) Nothing }

Structural Under

Under is a conjunction which applies a structural function, then another function, and then undoes the strucutral transformation. Extra care is taken for it to work with sorts when used as a transformation function. It is implemented by calling the transformation function with pairs of "marker" numbers and actual values.

under :: MonadError Error m => (Array -> m Array) -> (Array -> m Array) -> Array -> m Array
under f g arr = do
  let nums = fromJust $ arrayReshaped (arrayShape arr) $ Number . (:+ 0) <$> [1..]
  pairs <- atRank2 (atop enclose' pair) (0, 0) arr nums
  rs <- g pairs
  nums' <- atRank1 (compose TinyAPL.Functions.last first) 0 rs
  if Prelude.not $ distinct $ arrayContents rs then throwError $ DomainError "Under right operand must return each element at most once"
  else do
    res <- atRank1 (compose first first) 0 rs >>= f
    if isScalar res then do
      pure $ Array (arrayShape arr) $ zipWith (\num el -> if num `elem` (arrayContents nums') then headPromise $ arrayContents res else el) (arrayContents nums) (arrayContents arr)
    else if arrayShape nums' == arrayShape res then do
      let cs = zip (arrayContents nums') (arrayContents res)
      pure $ Array (arrayShape arr) $ zipWith (\num el -> case find (\(num', _) -> num == num') cs of
        Just (_, el') -> el'
        Nothing -> el) (arrayContents nums) (arrayContents arr)
    else throwError $ DomainError "Under left argument mustn't change the shape of the argument"

under2 :: MonadError Error m => (Array -> Array -> m Array) -> (Array -> m Array) -> Array -> Array -> m Array
under2 f g x = under (f x) g

underK :: MonadError Error m => Array -> (Array -> m Array) -> Array -> m Array
underK arr = under (\_ -> pure arr)

Matrix Inverse and Matrix Divide

These two functions are implemented using the matrix Haskell library.

matrixInverse :: MonadError Error m => M.Matrix (Complex Double) -> m (M.Matrix (Complex Double))
matrixInverse y = do
  let hermitian = fmap Cx.conjugate . M.transpose
  case M.inverse (hermitian y * y) of
    Left err -> throwError $ DomainError err
    Right r -> pure $ r * hermitian y

matrixInverse' :: MonadError Error m => Array -> m Array
matrixInverse' = atRank1 (\y -> do
  r <- rank y
  mat <- asMatrix (DomainError "") y >>= mapM (asNumber (DomainError "Matrix inverse argument must be numeric"))
  inv <- fmap Number <$> matrixInverse (if r < 2 then M.transpose mat else mat)
  if r < 2 then pure $ Array (arrayShape y) (M.toList inv)
  else pure $ matrix inv) 2

matrixDivide :: MonadError Error m => M.Matrix (Complex Double) -> M.Matrix (Complex Double) -> m (M.Matrix (Complex Double))
matrixDivide x y = (* x) <$> matrixInverse y

matrixDivide' :: MonadError Error m => Array -> Array -> m Array
matrixDivide' = atRank2 (\x y -> do
  x' <- asMatrix (DomainError "") x >>= mapM (asNumber (DomainError "Matrix divide arguments must be numeric"))
  y' <- asMatrix (DomainError "") y >>= mapM (asNumber (DomainError "Matrix divide arguments must be numeric")) 
  matrix . fmap Number <$> matrixDivide x' y') (2, 2)

On Cells and On Scalars

These two are just helper modifiers that are equivalent to applications of Rank: On Cells applies to rank -1, On Scalars to rank 0

onCells1 :: MonadError Error m => (Array -> m Array) -> Array -> m Array
onCells1 f = atRank1 f (-1)

onCells2 :: MonadError Error m => (Array -> Array -> m Array) -> Array -> Array -> m Array
onCells2 f = atRank2 f (-1, -1)

onScalars1 :: MonadError Error m => (Array -> m Array) -> Array -> m Array
onScalars1 f = atRank1 f 0

onScalars2 :: MonadError Error m => (Array -> Array -> m Array) -> Array -> Array -> m Array
onScalars2 f = atRank2 f (0, 0)

Boxed and On Contents

Boxed and On Contents are designed to break Each into more fundamental parts: apply to scalars, disclose arguments, enclose results.

boxed1 :: MonadError Error m => (Array -> m Array) -> Array -> m Array
boxed1 = compose enclose'

boxed2 :: MonadError Error m => (Array -> Array -> m Array) -> Array -> Array -> m Array
boxed2 = atop enclose'

onContents1 :: MonadError Error m => (Array -> m Array) -> Array -> m Array
onContents1 = (`compose` first)

onContents2 :: MonadError Error m => (Array -> Array -> m Array) -> Array -> Array -> m Array
onContents2 = (`over` first)

Table

Table is like other APL's Outer Product except it's flat instead of nested: while Outer Product can be defined as F¨⍤0‿∞, TinyAPL's Table is F◠⍤0‿∞.

table :: MonadError Error m => (Array -> Array -> m Array) -> Array -> Array -> m Array
table f = atRank2 (atRank2 f (0, 0)) (0, 10000)

Inner Product

Just like Table, Inner Product is flat instead of nested:

innerProduct :: MonadError Error m => (Array -> m Array) -> (Array -> Array -> m Array) -> Array -> Array -> m Array
innerProduct f g = atRank2 (atop f (atRank2 (atRank2 g (0, 0)) (-1, -1))) (1, 10000)

Factorial and Binomial

I would like to depend on the gamma library, but sadly it depends on Template Haskell and currently TH isn't supported on the WASM backend yet. Luckily, the changes required to get it working are minor so I just embedded the source. After that, Factorial is easy:

factorial :: MonadError Error m => ScalarValue -> m ScalarValue
factorial (Number n) = case asInt (DomainError "") n of
  Left _ -> pure $ Number $ Gamma.gamma $ n + 1
  Right i
    | i < 0 -> throwError $ DomainError "Factorial of a negative integer"
    | otherwise -> pure $ Number $ Gamma.factorial i
factorial _ = throwError expectedNumber

factorial' :: MonadError Error m => Array -> m Array
factorial' = scalarMonad factorial

Binomial is a bit more complex because it has many cases:

binomial :: MonadError Error m => ScalarValue -> ScalarValue -> m ScalarValue
binomial (Number x) (Number y) = let
  go :: MonadError Error m => Complex Double -> Complex Double -> m (Complex Double)
  go n k = do
    let ni = asInt (DomainError "") n :: Either Error Integer
    let ki = asInt (DomainError "") k :: Either Error Integer
    case (trace (show ni) ni, trace (show ki) ki) of
      (Right n', Right k')
        | n' < 0 && k' >= 0 -> (((-1) ^ k') *) <$> go (k - n - 1) k
        | n' < 0 && k' <= n' -> (((-1) ^ (n' - k')) *) <$> go (-k - 1) (n - k)
        | n' < 0 -> pure 0
      (Right n', _) | n' < 0 -> throwError $ DomainError "If Choose left argument is a negative integer, the right argument must be an integer"
      (Right n', Right k')
        | k' < 0 || k' > n' -> pure 0
        | otherwise -> pure $ Gamma.factorial n' / (Gamma.factorial k' * Gamma.factorial (n' - k'))
      _ -> pure $ Gamma.gamma (n + 1) / (Gamma.gamma (k + 1) * Gamma.gamma (n - k + 1))
  in Number <$> go y x
binomial _ _ = throwError expectedNumber

binomial' :: MonadError Error m => Array -> Array -> m Array
binomial' = scalarDyad binomial

Raise

Raise is a function that throws an error.

raise :: MonadError Error m => Int -> String -> m ()
raise = throwError .: fromErrorCode

raise' :: MonadError Error m => Array -> Array -> m Array
raise' code msg = do
  let err = DomainError "Raise left argument must be an integer scalar"
  code' <- asScalar err code >>= asNumber err >>= asInt err
  when (code' /= 0) $ raise code' $ show msg
  pure $ vector []

raise1 :: MonadError Error m => Array -> m Array
raise1 msg = do
  raise 1 $ show msg
  pure $ vector []

Type

⎕Type is a quad function that returns the type of the (scalars of the) argument.

type_ = Function (Just $ \(Array sh cs) -> return $ Array sh $ (\case
  Number _ -> Number 0
  Character _ -> Number 1
  Box _ -> Number 2
  Wrap _ -> Number 3
  AdverbWrap _ -> Number 4
  ConjunctionWrap _ -> Number 5
  Struct _ -> Number 6) <$> cs) Nothing (G.quad : "Type") Nothing

Closures

Currently, dfns have a weird system of capturing the outer scope where they take immutable references to copies of values. I changed that to instead make proper closing over lexical scopes, which includes noticing outside mutation and creation of new values. To accomplish this, functions (and modifiers) can optionally store a Context; when they're called, they're ran in the stored context if it exists; both Context's scope and Scope's parent scopes are also made IORef because they can be mutated.

data Function
  = Function
    { functionMonad :: Maybe (Array -> St Array)
    , functionDyad  :: Maybe (Array -> Array -> St Array)
    , functionRepr  :: String
    , functionContext :: Maybe Context }

callMonad (Function (Just f) _ _ (Just ctx)) x = runWithContext ctx $ f x
callMonad (Function (Just f) _ _ Nothing) x = f x
callMonad f@(Function Nothing _ _ _) _ = throwError $ noMonad $ show f

callDyad (Function _ (Just g) _ (Just ctx)) a b = runWithContext ctx $ g a b
callDyad (Function _ (Just g) _ Nothing) a b = g a b
callDyad f@(Function _ Nothing _ _) _ _ = throwError $ noDyad $ show f

-- same for modifiers
data Scope = Scope
  { scopeArrays :: [(String, Array)]
  , scopeFunctions :: [(String, Function)]
  , scopeAdverbs :: [(String, Adverb)]
  , scopeConjunctions :: [(String, Conjunction)]
  , scopeParent :: Maybe (IORef Scope) }

scopeLookupArray :: String -> Scope -> St (Maybe Array)
scopeLookupArray name sc = case lookup name (scopeArrays sc) of
  Just x -> pure $ Just x
  Nothing -> if name `elem` specialNames then pure Nothing else case scopeParent sc of
    Nothing -> pure Nothing
    Just p -> (liftToSt $ IORef.readIORef p) >>= scopeLookupArray name

-- same for other lookups

(and many other minor code changes to reflect the mutability changes)

Wraps

What TinyAPL calls "wraps" are essentially first-class functions, adverbs and conjunctions, that is, they're ScalarValues that contain a function or modifier. They're created with and unwrapped with /_⊐/_⊐_.

data ScalarValue
  = Number (Complex Double)
-- ...
  | Wrap Function
  | AdverbWrap Adverb
  | ConjunctionWrap Conjunction
data Token
  = TokenNumber (Complex Double) SourcePos
-- ...
  | TokenWrap Token SourcePos
  | TokenUnwrap Token SourcePos
  | TokenUnwrapAdverb Token SourcePos
  | TokenUnwrapConjunction Token SourcePos
		wrap :: Parser Token
    wrap = withPos $ TokenWrap <$> (char G.wrap *> bit)

		unwrap :: Parser Token
    unwrap = withPos $ TokenUnwrap <$> (char G.unwrap *> bit)

		unwrapAdverb :: Parser Token
    unwrapAdverb = withPos $ TokenUnwrapAdverb <$> (string [G.underscore, G.unwrap] *> bit)

		unwrapConjunction :: Parser Token
    unwrapConjunction = withPos $ TokenUnwrapConjunction <$> (string [G.underscore, G.unwrap, G.underscore] *> bit)
data Tree
  = Leaf { leafCategory :: Category, leafToken :: Token }
-- ...
  | WrapBranch { wrapBranchValue :: Tree }
  | UnwrapBranch { unwrapBranchCategory :: Category, unwrapBranchValue :: Tree }
	tokenToTree (TokenWrap val _)                             = WrapBranch <$> (tokenToTree val >>= (\x -> case treeCategory x of
    CatFunction -> pure x
    CatAdverb -> pure x
    CatConjunction -> pure x
    _ -> throwError $ makeSyntaxError (tokenPos val) source $ "Invalid wrap of type " ++ show (treeCategory x) ++ ", function, adverb or conjunction required"))
  tokenToTree (TokenUnwrap val _)                         = UnwrapBranch CatFunction <$> (tokenToTree val >>= requireOfCategory CatArray (\c -> makeSyntaxError (tokenPos val) source $ "Invalid unwrap of type " ++ show c ++ ", array required"))
  tokenToTree (TokenUnwrapAdverb val _)                   = UnwrapBranch CatAdverb <$> (tokenToTree val >>= requireOfCategory CatArray (\c -> makeSyntaxError (tokenPos val) source $ "Invalid unwrap adverb of type " ++ show c ++ ", array required"))
  tokenToTree (TokenUnwrapConjunction val _)              = UnwrapBranch CatConjunction <$> (tokenToTree val >>= requireOfCategory CatArray (\c -> makeSyntaxError (tokenPos val) source $ "Invalid unwrap conjunction of type " ++ show c ++ ", array required"))
eval (WrapBranch fn) = eval fn >>= (\case
  VFunction fn -> pure $ VArray $ scalar $ Wrap fn
  VAdverb adv -> pure $ VArray $ scalar $ AdverbWrap adv
  VConjunction conj -> pure $ VArray $ scalar $ ConjunctionWrap conj
  _ -> throwError $ DomainError "Wrap notation: function or modifier required")
eval (UnwrapBranch cat fn) = eval fn >>= evalUnwrap cat

asWraps :: MonadError Error m => Error -> Array -> m Function
asWraps err arr = do
  if null $ arrayShape arr then asWrap err (headPromise $ arrayContents arr)
  else pure $ Function
    { functionMonad = Just $ \x -> F.onScalars1 (\w -> asScalar err w >>= asWrap err >>= (\f -> callMonad f x)) arr
    , functionDyad = Just $ \x y -> F.onScalars1 (\w -> asScalar err w >>= asWrap err >>= (\f -> callDyad f x y)) arr
    , functionRepr = [fst G.parens, G.unwrap] ++ show arr ++ [snd G.parens]
    , functionContext = Nothing }

evalUnwrap :: Category -> Value -> St Value
evalUnwrap CatFunction v = do
  let err = DomainError "Unwrap notation: array of wraps required"
  VFunction <$> (unwrapArray err v >>= asWraps err)
evalUnwrap CatAdverb v = do
  let err = DomainError "Unwrap adverb notation: scalar array wrap required"
  arr <- unwrapArray err v
  if null $ arrayShape arr then VAdverb <$> asAdverbWrap err (headPromise $ arrayContents arr)
  else throwError err
evalUnwrap CatConjunction v = do
  let err = DomainError "Unwrap conjunction notation: scalar array wrap required"
  arr <- unwrapArray err v
  if null $ arrayShape arr then VConjunction <$> asConjunctionWrap err (headPromise $ arrayContents arr)
  else throwError err
evalUnwrap _ _ = throwError unreachable

Structs

Structs are similar to Dyalog namespaces, in that they're mutable containers of variables. They're created using and and are scalars. All statements inside the brackets are ran in a child scope and the assigned values are collected to be available for struct access, which is done using .

data ScalarValue
  = Number (Complex Double)
-- ...
  | Struct Context
data Token
  = TokenNumber (Complex Double) SourcePos
-- ...
  | TokenQualifiedArrayName Token (NonEmpty String) SourcePos
  | TokenQualifiedFunctionName Token (NonEmpty String) SourcePos
  | TokenQualifiedAdverbName Token (NonEmpty String) SourcePos
  | TokenQualifiedConjunctionName Token (NonEmpty String) SourcePos
-- ...
  | TokenQualifiedArrayAssign Token (NonEmpty String) (NonEmpty Token) SourcePos
  | TokenQualifiedFunctionAssign Token (NonEmpty String) (NonEmpty Token) SourcePos
  | TokenQualifiedAdverbAssign Token (NonEmpty String) (NonEmpty Token) SourcePos
  | TokenQualifiedConjunctionAssign Token (NonEmpty String) (NonEmpty Token) SourcePos
-- ...
  | TokenStruct [NonEmpty Token] SourcePos

    struct :: Parser Token
    struct = withPos $ TokenStruct <$> (string [fst G.struct] *> sepBy bits separator <* string [snd G.struct])
  
  maybeQualified ::[(Token -> NonEmpty String -> SourcePos -> Token, Token -> NonEmpty String -> AssignType -> NonEmpty Token -> SourcePos -> Token, Parser String)] -> Parser Token
  maybeQualified xs = do
    pos <- getSourcePos
    first <- bit'
    option first $ do
      lexeme $ char G.access
      (middle, (name, assign, last)) <- commitOn' (,) (many $ lexeme arrayName `commitOn` char G.access) (choice $ (\(n, a, p) -> (n, a, ) <$> p) <$> xs)
      option (name first (snocNE middle last) pos) $ do
        char G.assign
        w <- bits
        pure $ assign first (snocNE middle last) as w pos

  

array, function, adverb and conjunction have been split into array' et al which are the parts that can appear as the left side of a qualified access; bit' collects those; and array'' et al which collects the parts that cannot.

  bit' :: Parser Token
  bit' = lexeme $ bracketed <|> conjunction' <|> adverb' <|> function' <|> array'

  bit :: Parser Token
  bit = lexeme $ conjunction'' <|> adverb'' <|> function'' <|> array'' <|>
    maybeQualified
      [ (TokenQualifiedConjunctionName, TokenQualifiedConjunctionAssign, try conjunctionName)
      , (TokenQualifiedAdverbName, TokenQualifiedAdverbAssign, adverbName)
      , (TokenQualifiedFunctionName, TokenQualifiedFunctionAssign, functionName)
      , (TokenQualifiedArrayName, TokenQualifiedArrayAssign, arrayName) ]
    <|> conjunction' <|> adverb' <|> function' <|> array'
data Token
  = TokenNumber (Complex Double) SourcePos
-- ...
  | TokenQualifiedArrayName Token (NonEmpty String) SourcePos
  | TokenQualifiedFunctionName Token (NonEmpty String) SourcePos
  | TokenQualifiedAdverbName Token (NonEmpty String) SourcePos
  | TokenQualifiedConjunctionName Token (NonEmpty String) SourcePos
-- ...
  | TokenQualifiedArrayAssign Token (NonEmpty String) (NonEmpty Token) SourcePos
  | TokenQualifiedFunctionAssign Token (NonEmpty String) (NonEmpty Token) SourcePos
  | TokenQualifiedAdverbAssign Token (NonEmpty String) (NonEmpty Token) SourcePos
  | TokenQualifiedConjunctionAssign Token (NonEmpty String) (NonEmpty Token) SourcePos
-- ...
  | TokenStruct [NonEmpty Token] SourcePos

  tokenToTree (TokenQualifiedArrayName h ns _)              = qualified CatArray h ns
  tokenToTree (TokenQualifiedFunctionName h ns _)           = qualified CatFunction h ns
  tokenToTree (TokenQualifiedAdverbName h ns _)             = qualified CatAdverb h ns
  tokenToTree (TokenQualifiedConjunctionName h ns _)        = qualified CatConjunction h ns
  tokenToTree (TokenQualifiedArrayAssign h ns ts _)         = qualifiedAssignment CatArray h ns ts
  tokenToTree (TokenQualifiedFunctionAssign h ns ts _)      = qualifiedAssignment CatFunction h ns ts
  tokenToTree (TokenQualifiedAdverbAssign h ns ts _)        = qualifiedAssignment CatAdverb h ns ts
  tokenToTree (TokenQualifiedConjunctionAssign h ns ts _)   = qualifiedAssignment CatConjunction h ns ts
  tokenToTree (TokenStruct es pos)                          = struct es pos

  qualified :: Category -> Token -> NonEmpty String -> Result Tree
  qualified cat h ns = QualifiedBranch cat <$> (tokenToTree h >>=
    requireOfCategory CatArray (\c -> makeSyntaxError (tokenPos h) source $ "Invalid qualified access to value of type " ++ show c)) <*> pure ns

  qualifiedAssignment :: Category -> Token -> NonEmpty String -> NonEmpty Token -> Result Tree
  qualifiedAssignment cat h ns ts = liftA2 (\h' as -> QualifiedAssignBranch cat h' ns as) (tokenToTree h >>=
    requireOfCategory CatArray (\c -> makeSyntaxError (tokenPos h) source $ "Invalid qualified access to value of type " ++ show c)) (categorizeAndBind ts >>=
    requireOfCategory cat (\c -> makeSyntaxError (tokenPos $ NE.head ts) source $ "Invalid assignment of " ++ show c ++ " to " ++ show cat ++ " name"))
  
  struct :: [NonEmpty Token] -> SourcePos -> Result Tree
  struct es _ = StructBranch <$> mapM (\x -> categorizeAndBind x) es
eval (QualifiedBranch _ h ns) = eval h >>= flip evalQualified ns
eval (StructBranch es) = evalStruct es

evalQualified :: Value -> NonEmpty String -> St Value
evalQualified head ns = do
  let err = DomainError "Qualified name head should be a scalar struct"
  let (q, l) = unsnocNE ns
  headCtx <- unwrapArray err head >>= asScalar err >>= asStruct err
  ctx <- resolve headCtx q
  scope <- readRef $ contextScope ctx
  lift $ except $ maybeToEither (SyntaxError $ "Qualified variable " ++ l ++ " does not exist") $ scopeShallowLookup False l scope

evalQualifiedAssign :: Value -> NonEmpty String -> AssignType -> Value -> St Value
evalQualifiedAssign head ns c val = do
  let err = DomainError "Qualified name head should be a scalar struct"
  let (q, l) = unsnocNE ns
  headCtx <- unwrapArray err head >>= asScalar err >>= asStruct err
  ctx <- resolve headCtx q
  runWithContext ctx $ evalAssign True l c val

evalStruct :: [Tree] -> St Value
evalStruct statements = do
  ctx <- get
  newScope <- createRef $ Scope [] [] [] [] (Just $ contextScope ctx)
  let newContext = ctx{ contextScope = newScope }
  mapM_ (runWithContext newContext . eval) statements
  pure $ VArray $ scalar $ Struct newContext

Assignment Types

TinyAPL has four assignment types: normal , modify (goes through parent scopes to find the place where the name was defined), constant (cannot be reassigned) and private (in scopes, cannot be accessed from outside the scope).

The code for this is quite messy. It involves changing Scope entries to contain a VariableType (Normal, Constant or Private) and doing different lookups when resolving a qualified (non-private) name and an unqualified (potentially private) name; all assignment tokens and branches get a AssignType (Normal, Modify, Constant or Private). I don't think the code for this is particularly interesting, though, so I decided not to include it in this post.

Generalized Ties

In this section we add support for ties of arbitrary values instead of just number literals. First, TokenNumber is rolled back to store just one number.

data Token
  = TokenNumber (Complex Double) SourcePos
-- ...
  | TokenTie (NonEmpty Token) SourcePos

  maybeQualifiedTie :: (NonEmpty Token -> SourcePos -> Token) -> [(Token -> NonEmpty String -> SourcePos -> Token, Token -> NonEmpty String -> AssignType -> NonEmpty Token -> SourcePos -> Token, Parser String)] -> Parser Token
  maybeQualifiedTie tie xs = do
    pos <- getSourcePos
    first <- bit'
    choice [do
      lexeme $ char G.access
      (middle, (name, assign, last)) <- commitOn' (,) (many $ lexeme arrayName `commitOn` char G.access) (choice $ (\(n, a, p) -> (n, a, ) <$> p) <$> xs)
      option (name first (snocNE middle last) pos) $ do
        as <- assignArrow
        w <- bits
        pure $ assign first (snocNE middle last) as w pos, do
      lexeme $ char G.tie
      rest <- sepBy1 bit' (lexeme $ char G.tie)
      pure $ tie (first :| rest) pos, pure first]

  bit :: Parser Token
  bit = lexeme $ conjunction'' <|> adverb'' <|> function'' <|> array'' <|>
    maybeQualifiedTie
      TokenTie
      [ (TokenQualifiedConjunctionName, TokenQualifiedConjunctionAssign, try conjunctionName)
      , (TokenQualifiedAdverbName, TokenQualifiedAdverbAssign, adverbName)
      , (TokenQualifiedFunctionName, TokenQualifiedFunctionAssign, functionName)
      , (TokenQualifiedArrayName, TokenQualifiedArrayAssign, arrayName) ]
    <|> conjunction' <|> adverb' <|> function' <|> array'

Because ties and vector notation are equivalent, there is no new Branch for ties:

  tokenToTree (TokenTieAssign names c ts pos) = destructureAssignment VectorAssignBranch (NE.toList names) c ts pos

Destructuring Assignment

There are four types of destructuring assignment: vector assignment, high rank assignment, tie assignment (equivalent to vector assignment) and struct assignment.

data Token
  = TokenNumber (Complex Double) SourcePos
-- ...
  | TokenVectorAssign [String] AssignType (NonEmpty Token) SourcePos
  | TokenHighRankAssign [String] AssignType (NonEmpty Token) SourcePos
  | TokenTieAssign (NonEmpty String) AssignType (NonEmpty Token) SourcePos
  | TokenStructAssign [(String, Maybe (AssignType, String))] AssignType (NonEmpty Token) SourcePos
-- ...

    vectorAssign :: Parser Token
    vectorAssign = assign' TokenVectorAssign $ between (lexeme $ char $ fst G.vector) (lexeme $ char $ snd G.vector) (sepBy (lexeme arrayName) separator)

    highRankAssign :: Parser Token
    highRankAssign = assign' TokenHighRankAssign $ between (lexeme $ char $ fst G.highRank) (lexeme $ char $ snd G.highRank) (sepBy (lexeme arrayName) separator)

    tieAssign :: Parser Token
    tieAssign = assign' TokenTieAssign $ liftA2 (:|) (lexeme arrayName `commitOn` lexeme (char G.tie)) (sepBy (lexeme arrayName) (lexeme $ char G.tie))

    structAssign :: Parser Token
    structAssign = assign' TokenStructAssign $ between (lexeme $ char $ fst G.struct) (lexeme $ char $ snd G.struct) (sepBy (liftA2 (,) (lexeme anyName) (option Nothing $ Just <$> liftA2 (,) (lexeme assignArrow) (lexeme anyName))) separator)
data Tree
  = Leaf { leafCategory :: Category, leafToken :: Token }
-- ...
  | VectorAssignBranch { vectorAssignBranchNames :: [String], vectorAssignBranchType :: AssignType, vectorAssignBranchValue :: Tree }
  | HighRankAssignBranch { highRankAssignBranchNames :: [String], highRankAssignBranchType :: AssignType, highRankAssignBranchValue :: Tree }
  | StructAssignBranch { structAssignBranchNames :: [(String, Maybe (AssignType, String))], structAssignBranchType :: AssignType, structAssignBranchValue :: Tree }
-- ...

  tokenToTree (TokenVectorAssign names c ts pos)            = destructureAssignment VectorAssignBranch names c ts pos
  tokenToTree (TokenHighRankAssign names c ts pos)          = destructureAssignment HighRankAssignBranch names c ts pos
  tokenToTree (TokenTieAssign names c ts pos)               = destructureAssignment VectorAssignBranch (NE.toList names) c ts pos
  tokenToTree (TokenStructAssign names c ts pos)            = structAssignment names c ts pos

  destructureAssignment :: ([String] -> AssignType -> Tree -> Tree) -> [String] -> AssignType -> NonEmpty Token -> SourcePos -> Result Tree
  destructureAssignment h names ty ts pos = h names ty <$> (categorizeAndBind ts >>= requireOfCategory CatArray (\c -> makeSyntaxError pos source $ "Invalid destructure assignment of " ++ show c ++ ", array required"))

  structAssignment :: [(String, Maybe (AssignType, String))] -> AssignType -> NonEmpty Token -> SourcePos -> Result Tree
  structAssignment names ty ts pos = do
    let ns = mapMaybe (\case { (_, Nothing) -> Nothing; (n, Just (_, n')) -> Just (n, n') }) names
    mapM_ (\(n, n') -> if not $
      (isArrayName n && isArrayName n') ||
      (isFunctionName n && isFunctionName n') ||
      (isAdverbName n && isAdverbName n') ||
      (isConjunctionName n && isConjunctionName n')
      then throwError $ makeSyntaxError pos source $ "Struct assignment: same type required for both the original and aliased name"
      else pure ()) ns
    StructAssignBranch names ty <$> (categorizeAndBind ts >>= requireOfCategory CatArray (\c -> makeSyntaxError pos source $ "Invalid struct assignment of " ++ show c ++ ", array required"))
eval (VectorAssignBranch ns c val) = eval val >>= evalVectorAssign ns c
eval (HighRankAssignBranch ns c val) = eval val >>= evalHighRankAssign ns c
eval (StructAssignBranch ns c val) = eval val >>= evalStructAssign ns c

evalVectorAssign :: [String] -> AssignType -> Value -> St Value
evalVectorAssign ns c val =
  if any (\(n:_) -> n == G.quad || n == G.quadQuote) ns then throwError $ DomainError "Vector assignment: cannot assign to quad names"
  else do
    es <- fmap fromScalar <$> (unwrapArray (DomainError "Vector assign: not a vector") val >>= asVector (DomainError "Vector assignment: not a vector"))
    if length ns /= length es then throwError $ DomainError "Vector assignment: wrong number of names"
    else zipWithM_ (\name value -> evalAssign False True name c (VArray value)) ns es $> val

evalHighRankAssign :: [String] -> AssignType -> Value -> St Value
evalHighRankAssign ns c val =
  if any (\(n:_) -> n == G.quad || n == G.quadQuote) ns then throwError $ DomainError "High rank assignment: cannot assign to quad names"
  else do
    es <- majorCells <$> unwrapArray (DomainError "High rank assign: not an array") val
    if length ns /= length es then throwError $ DomainError "High rank assignment: wrong number of names"
    else zipWithM_ (\name value -> evalAssign False True name c (VArray value)) ns es $> val

evalStructAssign :: [(String, Maybe (AssignType, String))] -> AssignType -> Value -> St Value
evalStructAssign ns c val = do
  let err = DomainError "Struct assignment: not a struct"
  s <- unwrapArray err val >>= asScalar err >>= asStruct err >>= readRef . contextScope
  mapM_ (\(name, alias) -> do {
      let (n, t) = case alias of {
          Nothing -> (name, c)
        ; Just ((t', n')) -> (n', t') }
    ; let v = scopeShallowLookup False n s
    ; case v of
        Nothing -> throwError $ SyntaxError $ "Struct assignment: variable " ++ n ++ " does not exist"
        Just v' -> evalAssign False True name t v'
    ; pure () }) ns
  pure val

Removing Circular

Circular is a weird function. It's sometimes called a "trenchcoat function": a function that does unrelated things based on one of the arguments. I dropped it in favor of functions in ⎕math, a quad struct.

piA :: Array
piA = scalar $ Number $ pi

complementaryS :: MonadError Error m => ScalarValue -> m ScalarValue
complementaryS (Number y) = pure $ Number $ sqrt $ 1 - y * y
complementaryS _ = throwError $ DomainError "Complementary argument must be a number"

complementaryF :: Function
complementaryF = Function (Just $ scalarMonad complementaryS) Nothing "Complementary" Nothing

sinS :: MonadError Error m => ScalarValue -> m ScalarValue
sinS (Number y) = pure $ Number $ sin y
sinS _ = throwError $ DomainError "Sine argument must be a number"

sinF :: Function
sinF = Function (Just $ scalarMonad sinS) Nothing "Sin" Nothing

arcsinS :: MonadError Error m => ScalarValue -> m ScalarValue
arcsinS (Number y) = pure $ Number $ asin y
arcsinS _ = throwError $ DomainError "Arcsine argument must be a number"

arcsinF :: Function
arcsinF = Function (Just $ scalarMonad arcsinS) Nothing "Arcsin" Nothing

cosS :: MonadError Error m => ScalarValue -> m ScalarValue
cosS (Number y) = pure $ Number $ cos y
cosS _ = throwError $ DomainError "Cosine argument must be a number"

cosF :: Function
cosF = Function (Just $ scalarMonad cosS) Nothing "Cos" Nothing

arccosS :: MonadError Error m => ScalarValue -> m ScalarValue 
arccosS (Number y) = pure $ Number $ acos y
arccosS _ = throwError $ DomainError "Arccosine argument must be a number"

arccosF :: Function
arccosF = Function (Just $ scalarMonad arccosS) Nothing "Arccos" Nothing

tanS :: MonadError Error m => ScalarValue -> m ScalarValue  
tanS (Number y) = pure $ Number $ tan y
tanS _ = throwError $ DomainError "Tangent argument must be a number"

tanF :: Function
tanF = Function (Just $ scalarMonad tanS) Nothing "Tan" Nothing

arctanS :: MonadError Error m => ScalarValue -> m ScalarValue  
arctanS (Number y) = pure $ Number $ atan y
arctanS _ = throwError $ DomainError "Arctangent argument must be a number"

arctanF :: Function
arctanF = Function (Just $ scalarMonad arctanS) Nothing "Arctan" Nothing

sinhS :: MonadError Error m => ScalarValue -> m ScalarValue  
sinhS (Number y) = pure $ Number $ sinh y
sinhS _ = throwError $ DomainError "Hyperbolic sine argument must be a number"

sinhF :: Function
sinhF = Function (Just $ scalarMonad sinhS) Nothing "Sinh" Nothing

arsinhS :: MonadError Error m => ScalarValue -> m ScalarValue  
arsinhS (Number y) = pure $ Number $ asinh y
arsinhS _ = throwError $ DomainError "Hyperbolic arsine argument must be a number"

arsinhF :: Function
arsinhF = Function (Just $ scalarMonad arsinhS) Nothing "Arsinh" Nothing

coshS :: MonadError Error m => ScalarValue -> m ScalarValue  
coshS (Number y) = pure $ Number $ cosh y
coshS _ = throwError $ DomainError "Hyperbolic cosine argument must be a number"

coshF :: Function
coshF = Function (Just $ scalarMonad coshS) Nothing "Cosh" Nothing

arcoshS :: MonadError Error m => ScalarValue -> m ScalarValue  
arcoshS (Number y) = pure $ Number $ acosh y
arcoshS _ = throwError $ DomainError "Hyperbolic arcosine argument must be a number"

arcoshF :: Function
arcoshF = Function (Just $ scalarMonad arcoshS) Nothing "Arcosh" Nothing

tanhS :: MonadError Error m => ScalarValue -> m ScalarValue  
tanhS (Number y) = pure $ Number $ tanh y
tanhS _ = throwError $ DomainError "Hyperbolic tangent argument must be a number"

tanhF :: Function
tanhF = Function (Just $ scalarMonad tanhS) Nothing "Tanh" Nothing

artanhS :: MonadError Error m => ScalarValue -> m ScalarValue  
artanhS (Number y) = pure $ Number $ atanh y
artanhS _ = throwError $ DomainError "Hyperbolic artangent argument must be a number"

artanhF :: Function
artanhF = Function (Just $ scalarMonad artanhS) Nothing "Artanh" Nothing

math = Nilad (Just $ do
  scope <- createRef (Scope [("pi", (VariableConstant, piA))] ((\n -> (functionRepr n, (VariableConstant, n))) <$>
    [ complementaryF
    , sinF
    , arcsinF
    , cosF
    , arccosF
    , tanF
    , arctanF
    , sinhF
    , arsinhF
    , coshF
    , arcoshF
    , tanhF
    , artanhF
    ]) [] [] Nothing)
  ctx <- get
  pure $ scalar $ Struct $ ctx{ contextScope = scope } ) Nothing (G.quad : "math") Nothing

Standard Library

TinyAPL now has a standard library! It's currently very minimal, there is a math module with a reimplementation of circular and some constants; bitwise with bitwise operations; prototype with the beginning of a pseudo-prototype system that allows for overtaking.

The standard library is embedded in the executable (in TinyAPL.StandardLibrary) using Template Haskell on platforms where it's supported, and on WASM using a global initializer that gets called by a C function that's exported as the initialization function for Wizer.

#ifdef wasm32_HOST_ARCH
standardLibrary :: [([String], String)]
standardLibrary = unsafePerformIO $ listToStd <$> fileList "/std"
{-# NOINLINE standardLibrary #-}

foreign export ccall loadStandardLibrary :: IO ()

loadStandardLibrary :: IO ()
loadStandardLibrary = void $ evaluate $ rnf standardLibrary
#else
standardLibrary :: [([String], String)]
standardLibrary = listToStd $ $(ListE <$> ((runIO $ fileList "std") >>= mapM (pairToExp "std")))
#endif

Import

⎕Import is a quad name that runs a file and returns its globals in a struct. Because each system has a different notion of reading files, instead of being a normal quad name there is a function that constructs a Function for importing. It also supports overriding the standard library lookup with the default.

makeImport :: (FilePath -> St String) -> Maybe ([String] -> St String) -> Function
makeImport read readStd = Function (Just $ \x -> do
  let err = DomainError "Import argument must be a character vector"
  path <- asVector err x >>= mapM (asCharacter err)
  ctx <- getContext
  scope <- createRef $ Scope [] [] [] [] Nothing -- The scope has intentionally no parent; imports run in an isolated context
  let ctx' = ctx{ contextScope = scope }
  source <-
    if isPrefixOf "std:" path
    then case readStd of
      Just fn -> fn <$> splitOn "/" $ drop (length "std:") path
      Nothing -> case lookup (splitOn "/" $ drop (length "std:") path) standardLibrary of
        Just source -> pure source
        Nothing -> throwError $ DomainError $ "Standard library module " ++ path ++ " not found"
    else read path
  runWithContext ctx' $ run' path source
  pure $ scalar $ Struct ctx') Nothing (G.quad : "Import") Nothing

Done!

That's it, you've finally reached the end of the article! TinyAPL is, in my opinion, way more usable now, so it's a great time to try it out.