Back to Index Page

TinyAPL part 7: Quads, Key, Index

#tinyapl#apl#haskell

Madeline Vergani

As usual, code is available in a GitHub repository.

Refactoring

To make it easier for functions to use implementations of other functions, I extracted all the functions from the pureFunction arguments and generalized them to take any MonadError Error m instead of plain Result/ResultIO/St. Most of the functions have a version which operates on the most natural Haskell datatype, and a version labeled with a ' which takes arrays and does error checking.

Scalar functions generally look like this:

conjugate :: MonadError Error m => ScalarValue -> m ScalarValue
conjugate (Number y) = pure $ Number $ Cx.conjugate y
conjugate _ = throwError expectedNumber

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

And here's a non-scalar function:

reshape :: MonadError Error m => [Integer] -> Array -> m Array
reshape shape arr@(Array _ xs) = do
  let negative = count (< 0) shape
  if negative == 0 then case arrayReshaped (fromInteger . toInteger <$> shape) xs of
    Nothing -> throwError $ DomainError "Cannot reshape an empty array to a non-empty array"
    Just res -> pure res
  else if negative == 1 && -1 `elem` shape then do
    let bound = genericLength xs
    let known = product $ filter (>= 0) shape
    if known == 0 then throwError $ DomainError "Shape cannot contain both 0 and -1"
    else if bound `mod` known /= 0 then throwError $ DomainError "Shape is not a multiple of the bound of the array"
    else reshape ((\x -> if x == -1 then bound `div` known else x) <$> shape) arr
  else throwError $ DomainError "Invalid shape"

reshape' :: MonadError Error m => Array -> Array -> m Array
reshape' sh arr = do
  let err = DomainError "Shape must be an integer vector"
  shape <- asVector err sh >>= mapM (asNumber err >=> asInt err)
  reshape shape arr

Similarly, all operators have been extracted into functions and the Function enum has been reduced to a simple record.

compose :: MonadError Error m => (b -> m c) -> (a -> m b) -> a -> m c
compose f g = g >=> f

atop :: MonadError Error m => (c -> m d) -> (a -> b -> m c) -> a -> b -> m d
atop f g x y = g x y >>= f

atop = Conjunction
  { conjRepr = [G.atop]
  , conjOnArrayArray = Nothing
  , conjOnArrayFunction = Nothing
  , conjOnFunctionArray = Just $ \_ _ -> throwError $ NYIError "Rank operator not implemented yet"
  , conjOnFunctionFunction = Just $ \f g -> pure $ Function (Just $ F.compose (callMonad f) (callMonad g)) (Just $ F.atop (callMonad f) (callDyad g)) (makeConjRepr (show f) G.atop (show g)) }

Affine character arithmetic

Affine character arithmetic is the ability to apply certain arithmetic functions on the codepoints of characters:

add (Number x) (Character y) = do
  x' <- asInt expectedInteger x
  pure $ Character $ chr $ ord y + x'
add (Character x) (Number y) = do
  y' <- asInt expectedInteger y
  pure $ Character $ chr $ ord x + y'

sub (Character x) (Number y) = do
  y' <- asInt expectedInteger y
  pure $ Character $ chr $ ord x - y'
sub (Character x) (Character y) = pure $ Number $ fromInteger . toInteger $ ord x - ord y

Supporting quad names

First, changing the parser to accept quad names:

		arrayName = try (liftA3 (\x y z -> x : y : z) (char G.quad) (oneOf arrayStart) (many $ oneOf identifierRest)) <|> try (string [G.alpha, G.alpha]) <|> try (string [G.omega, G.omega]) <|> try (string [G.alpha]) <|> try (string [G.omega]) <|> try (string [G.quad]) <|> try (string [G.quadQuote]) <|> liftA2 (:) (oneOf arrayStart) (many $ oneOf identifierRest)

		functionName = try (liftA3 (\x y z -> x : y : z) (char G.quad) (oneOf functionStart) (many $ oneOf identifierRest)) <|> try (string [G.del]) <|> try (string [G.alphaBar, G.alphaBar]) <|> try (string [G.omegaBar, G.omegaBar]) <|> liftA2 (:) (oneOf functionStart) (many $ oneOf identifierRest)

		adverbName = try (liftA3 (\x y z -> x : y : z) (char G.quad) (char G.underscore) (many $ oneOf identifierRest)) <|> try (string [G.underscore, G.del]) <|> liftA2 (:) (char G.underscore) (many1 $ oneOf identifierRest)

		conjunctionName = try ((\x y z w -> x : y : z ++ [w]) <$> char G.quad <*> char G.underscore <*> many (oneOf identifierRest) <*> char G.underscore) <|> try (string [G.underscore, G.del, G.underscore]) <|> liftA3 (\a b c -> a : b ++ [c]) (char G.underscore) (many1 $ oneOf identifierRest) (char G.underscore)

Quad array names are special in that accessing them may yield a non-constant value and setting them can trigger side effects; also some quad names can only be get and others can only be set:

data Nilad = Nilad
  { niladGet :: Maybe (St Array)
  , niladSet :: Maybe (Array -> St ())
  , niladRepr :: String }

instance Show Nilad where 
  show (Nilad { niladRepr = r }) = r

data Quads = Quads
  { quadArrays :: [(String, Nilad)]
  , quadFunctions :: [(String, Function)]
  , quadAdverbs :: [(String, Adverb)]
  , quadConjunctions :: [(String, Conjunction)] }
  deriving (Show)

Quad names are not intrinsic to the language as primitives are, instead different contexts can use different quad names. They are stored as a member of the Scope:

data Scope = Scope
  { scopeArrays :: [(String, Array)]
  , scopeFunctions :: [(String, Function)]
  , scopeAdverbs :: [(String, Adverb)]
  , scopeConjunctions :: [(String, Conjunction)]
  , scopeParent :: Maybe Scope
  , scopeQuads :: Quads }

Next, the interpreter has to recognize quad names and call them instead of looking up a name in the normal scope table:

evalLeaf (TokenArrayName name _)
	-- ...
	| head name == G.quad                = do
    quads <- gets scopeQuads
    let nilad = lookup name $ quadArrays quads
    case nilad of
      Just x -> case niladGet x of
        Just g -> VArray <$> g
        Nothing -> throwError $ SyntaxError $ "Quad name " ++ name ++ " cannot be accessed"
      Nothing -> throwError $ SyntaxError $ "Unknown quad name " ++ name

evalLeaf (TokenFunctionName name _)
  | head name == G.quad                = do
    quads <- gets scopeQuads
    let fn = lookup name $ quadFunctions quads
    case fn of
      Just x -> return $ VFunction x
      Nothing -> throwError $ SyntaxError $ "Unknown quad name " ++ name

evalLeaf (TokenAdverbName name _)
  | head name == G.quad                = do
    quads <- gets scopeQuads
    let adv = lookup name $ quadAdverbs quads
    case adv of
      Just x -> return $ VAdverb x
      Nothing -> throwError $ SyntaxError $ "Unknown quad name " ++ name

evalLeaf (TokenConjunctionName name _)
  | head name == G.quad                = do
    quads <- gets scopeQuads
    let conj = lookup name $ quadConjunctions quads
    case conj of
      Just x -> return $ VConjunction x
      Nothing -> throwError $ SyntaxError $ "Unknown quad name " ++ name

Similarly for array assignment:

  | head name == G.quad   = do
    arr <- unwrapArray (DomainError "Cannot set quad name to non-array") val
    quads <- gets scopeQuads
    let nilad = lookup name $ quadArrays quads
    case nilad of
      Just x -> case niladSet x of
        Just s -> do
          s arr
          return val
        Nothing -> throwError $ SyntaxError $ "Quad name " ++ name ++ " cannot be set"
      Nothing -> throwError $ SyntaxError $ "Unknown quad name " ++ name

Implementing quad names

Now that the structure is in place, quad names can be implemented. For now, I did the easy ones:

module TinyAPL.CoreQuads where

-- ...

io = Nilad (Just $ pure $ scalar $ Number 1) Nothing (G.quad : "io")
ct = Nilad (Just $ pure $ scalar $ Number $ comparisonTolerance :+ 0) Nothing (G.quad : "ct")
u = Nilad (Just $ pure $ vector $ Character <$> ['A'..'Z']) Nothing (G.quad : "u")
l = Nilad (Just $ pure $ vector $ Character <$> ['a'..'z']) Nothing (G.quad : "l")
d = Nilad (Just $ pure $ vector $ Character <$> ['0'..'9']) Nothing (G.quad : "d")

exists = Function (Just $ \x -> do
  let var = show x
  scope <- get
  case scopeLookup var scope of
    Just _ -> return $ scalar $ Number 1
    Nothing -> return $ scalar $ Number 0
  ) Nothing (G.quad : "Exists")

core = Quads
  ((\x -> (niladRepr x, x)) <$>
  [ io, ct, u, l, d ])
  ((\x -> (dfnRepr x, x)) <$>
  [ exists ])
  ((\x -> (adverbRepr x, x)) <$>
  [])
  ((\x -> (conjRepr x, x)) <$>
  [])

⎕Repr: representing arrays

The ⎕Repr quad name serializes an array into a format that can be read by ⍎ (which I didn't implement yet).

repr = Function (Just $ \x -> return $ vector $ Character <$> arrayRepr x) Nothing (G.quad : "Repr")
charRepr :: Char -> (String, Bool)
charRepr c = case lookup c (swap <$> G.escapes) of
  Just e -> ([G.stringEscape, e], True)
  Nothing -> ([c], False)

scalarRepr :: ScalarValue -> String
scalarRepr (Number x) = showComplex x
scalarRepr (Character x) = case charRepr x of
  (e, True) -> [G.first, G.stringDelimiter] ++ e ++ [G.stringDelimiter]
  (c, False) -> [G.charDelimiter] ++ c ++ [G.charDelimiter]
scalarRepr (Box xs) = G.enclose : arrayRepr xs

stringRepr :: [Char] -> String
stringRepr str = [G.stringDelimiter] ++ concatMap (fst . charRepr) str ++ [G.stringDelimiter]

arrayRepr :: Array -> String
arrayRepr (Array [] [s]) = scalarRepr s
arrayRepr (Array [_] xs)
  | not (null xs) && all (\case (Character _) -> True; _ -> False) xs = stringRepr $ (\case (Character c) -> c; _ -> undefined) <$> xs
  | otherwise = [fst G.vector] ++ intercalate [' ', G.separator, ' '] (arrayRepr . fromScalar <$> xs) ++ [snd G.vector]
arrayRepr arr = [fst G.highRank] ++ intercalate [' ', G.separator, ' '] (arrayRepr <$> majorCells arr) ++ [snd G.highRank]

Decoupling array functions from Result

Another big refactoring: array functions can now accept any MonadError Error m.

asScalar :: MonadError Error m => Error -> Array -> m ScalarValue
asScalar _ (Array _ [x]) = pure x
asScalar e _ = throwError e

-- et cetera

[!] Roll and Seed

[!] All the code in this section has temporarily been removed as the random package does not compile on the WASM backend.

Roll ? uses random as a dependency to generate random numbers; therefore it requires a MonadIO as the running context and not just a MonadError Error.

roll :: (MonadError Error m, MonadIO m) => Natural -> m Double
roll y =
  if y == 0 then randomRIO (0, 1)
  else fromInteger <$> randomRIO (1, toInteger y)

roll' :: (MonadError Error m, MonadIO m) => Array -> m Array
roll' = scalarMonad $ \y -> do
  n <- asNumber expectedNatural y >>= asNat expectedNatural
  Number . (:+ 0) <$> roll n

⎕seed is a set-only quad array that is used to seed the random generator:

seed = Nilad Nothing (Just $ \x -> do
  let e = DomainError "Seed must be a scalar integer"
  seed <- liftEither (asScalar e x) >>= liftEither . asNumber e >>= liftEither . asInt e
  setStdGen $ mkStdGen seed) (G.quad : "seed")

Removing the Set * functions

Set Phase, Set Real Part and Set Imaginary part have all been removed. I think I'm still in a phase where I can remove things without warning (though I did put a deprecation notice for a few days in the docs), it's not like anybody uses TinyAPL anyways.

Changing Reduce and Scan

Scans have been removed in favor of new On Prefixes ↟ and On Suffixes ↡ adverbs, which generalize scans to non-reduction functions. Reduces have had their name and glyph changed (Reduce Down → Reduce ⍆, Reduce Up → Reduce Back ⍅).

onPrefixes :: MonadError Error m => ([a] -> m b) -> [a] -> m [b]
onPrefixes f = mapM f . prefixes

onPrefixes' :: MonadError Error m => (Array -> m Array) -> Array -> m Array
onPrefixes' f arr = fromMajorCells <$> onPrefixes (f . fromMajorCells) (majorCells arr)

onSuffixes :: MonadError Error m => ([a] -> m b) -> [a] -> m [b]
onSuffixes f = mapM f . suffixes

onSuffixes' :: MonadError Error m => (Array -> m Array) -> Array -> m Array
onSuffixes' f arr = fromMajorCells <$> onSuffixes (f . fromMajorCells) (majorCells arr)

Key

Key ⌸ is an adverb that calls a function on each unique value of the left argument and the corresponding values of the right argument.

group :: Eq k => [k] -> [a] -> [(k, [a])]
group [] [] = []
group (k:ks) (a:as) = let gs = group ks as in case lookup k gs of
  Just g -> (k, a:g) : delete gs k
  Nothing -> (k, [a]) : gs
group _ _ = error "group: mismatched array lengths"
key :: (Eq a, MonadError Error m) => (a -> [b] -> m c) -> [a] -> [b] -> m [c]
key f ks vs
  | length ks == length vs = mapM (uncurry f) (group ks vs)
  | otherwise = throwError $ LengthError "Incompatible shapes to Key"

key' :: MonadError Error m => (Array -> Array -> m Array) -> Array -> Array -> m Array
key' f karr varr = fromMajorCells <$> key (\k vs -> f k $ vector $ toScalar <$> vs) (majorCells karr) (majorCells varr)

keyMonad :: MonadError Error m => (Array -> Array -> m Array) -> Array -> m Array
keyMonad f arr = do
  t <- tally' arr
  is <- indexGenerator' t
  key' f arr is

Indexing

Two indexing functions are available in TinyAPL: From ⊇ and (squad) Index ⌷.

Squad indexing is easier:

indexCells :: MonadError Error m => [Integer] -> [a] -> m [a]
indexCells [] _ = pure []
indexCells (i:is) xs
  | i < 0 = indexCells (genericLength xs + i + 1 : is) xs
  | i == 0 || i > genericLength xs = throwError $ DomainError "Index out of bounds"
  | otherwise = (genericIndex xs (i - 1) :) <$> indexCells is xs

indexDeep :: MonadError Error m => [[Integer]] -> Array -> m Array
indexDeep [] arr = pure arr
indexDeep (i:is) arr = indexCells i (majorCells arr) >>= (fmap (\x -> if length x == 1 then head x else fromMajorCells x) . mapM (indexDeep is))

squad :: MonadError Error m => Array -> Array -> m Array
squad iarr carr = do
  let err = DomainError "Squad left argument must be a vector of scalar integers or vectors of integers"
  is <- asVector err iarr >>= mapM (asVector err >=> mapM (asNumber err >=> asInt err)) . fmap fromScalar
  indexDeep is carr

From is more complex; it could be easily implemented using Rank but I don't have that yet. TODO: refactor!

indexScatter :: MonadError Error m => Array -> Array -> m Array
indexScatter iarr carr = each1 (\i -> do
  let err = DomainError "From left argument must be an integer vector or array of integer vectors"
  is <- asVector err i >>= mapM (asNumber err >=> asInt err)
  indexDeep (singleton <$> is) carr) iarr

from :: MonadError Error m => Array -> Array -> m Array
from iarr carr
  | null (arrayShape iarr) && all (\case { Number _ -> True; _ -> False }) (arrayContents iarr) = do
    let err = DomainError "From left argument must be an integer vector or an array of integer vectors"
    index <- asScalar err iarr >>= asNumber err >>= asInt err
    head <$> indexCells [index] (majorCells carr)
  | length (arrayShape iarr) == 1 && all (\case { Number _ -> True; _ -> False }) (arrayContents iarr) = do
    let err = DomainError "From left argument must be an integer vector or an array of integer vectors"
    indices <- asVector err iarr >>= mapM (asNumber err >=> asInt err)
    fromMajorCells <$> indexCells indices (majorCells carr)
  | otherwise = indexScatter iarr carr