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]
Result
Decoupling array functions from 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