Now that TinyAPL is usable, I've been writing lots of code in it to test it and the functions I've been definitely missing the most are the base conversion functions (Encode and Decode) and the search functions (Element Of and Index Of, mostly). Let's fix that!
Just as usual, all the code here is available on GitHub.
Ternaries
Ternaries are just like those in JavaScript or whatever: based on a condition, return one of two expressions. They use the glyphs ⍰
and ⍠
.
data Token
= TokenNumber (Complex Double) SourcePos
-- ...
| TokenTernary (NonEmpty Token) (NonEmpty Token) (NonEmpty Token) SourcePos
bits = spaceConsumer *> (do
one <- NE.some1 bit
option one $ fmap NE.singleton $ withPos $ do
char $ fst G.ternary
two <- NE.some1 bit
char $ snd G.ternary
three <- NE.some1 bit
pure $ TokenTernary one two three)
data Tree
= Leaf { leafCategory :: Category, leafToken :: Token }
-- ...
| TernaryBranch { ternaryBranchCondition :: Tree, ternaryBranchTrue :: Tree, ternaryBranchFalse :: Tree }
ternary :: NonEmpty Token -> NonEmpty Token -> NonEmpty Token -> Result Tree
ternary h t f = do
cond <- categorizeAndBind h >>= requireOfCategory CatArray (\c -> makeSyntaxError (tokenPos $ NE.head h) source $ "Invalid ternary condition of type " ++ show c ++ ", array required")
true <- categorizeAndBind t >>= requireOfCategory CatArray (\c -> makeSyntaxError (tokenPos $ NE.head t) source $ "Invalid ternary true of type " ++ show c ++ ", array required")
false <- categorizeAndBind f >>= requireOfCategory CatArray (\c -> makeSyntaxError (tokenPos $ NE.head f) source $ "Invalid ternary false of type " ++ show c ++ ", array required")
pure $ TernaryBranch cond true false
tokenToTree (TokenTernary c t f _) = ternary c t f
eval (TernaryBranch cond true false) = do
let err = DomainError "Ternary condition must be a scalar boolean"
c <- eval cond >>= unwrapArray err >>= asScalar err >>= asBool err
if c then eval true else eval false
Note that the branch that doesn't get selected doesn't even get evaluated! This means you can refer to potentially undefined variables that you checked for in the condition.
System Info
⎕systemInfo
is a very simple quad struct that returns the OS and architecture (as returned by System.Info
, for now) and whether the code is being ran in the JS interpreter. Just like ⎕Import
, it is exported as a constructor to allow for different executables to pass different inputs.
makeSystemInfo :: String -> String -> Bool -> Nilad
makeSystemInfo os arch js = Nilad (Just $ do
scope <- createRef $ Scope [ ("os", (VariableConstant, vector $ Character <$> os))
, ("arch", (VariableConstant, vector $ Character <$> arch))
, ("js", (VariableConstant, scalar $ boolToScalar js))] [] [] [] Nothing
ctx <- get
pure $ scalar $ Struct $ ctx{ contextScope = scope } ) Nothing (G.quad : "systemInfo") Nothing
Timestamp
While ⎕unix
returns the current Unix timestamp, ⎕ts
returns a more user-friendly value: a vector of year, month, day, hours, minutes, seconds and milliseconds.
fixedToFractional :: (Fractional b, HasResolution r) => Fixed r -> b
fixedToFractional f@(MkFixed v) = fromIntegral v / fromIntegral (resolution f)
ts = Nilad (Just $ do
utc <- liftToSt $ posixSecondsToUTCTime <$> getPOSIXTime
let (YearMonthDay y m d) = utctDay utc
let (TimeOfDay h min s') = timeToTimeOfDay $ utctDayTime utc
let s = floor $ fixedToFractional s'
let ms = round $ fracPart (fixedToFractional s') * 1000
pure $ vector [Number $ fromIntegral y, Number $ fromIntegral m, Number $ fromIntegral d, Number $ fromIntegral h, Number $ fromIntegral min, Number $ fromIntegral s, Number $ fromIntegral ms]) Nothing (G.quad : "ts") Nothing
Decode
Decode and Base 2 Decode ⊥
are two related functions (the latter is the former with a default argument) which convert a number in base representation back to its original number. In the most general form, the left argument is a mixed base vector which defines the value of each position in the right "coefficients" vector. Alternatively, the left argument can be a scalar and it is replicated for each element of the right vector.
decode :: MonadError Error m => [Complex Double] -> [Complex Double] -> m (Complex Double)
decode ns cs =
if length ns /= length cs then throwError $ LengthError "Decode arguments must have the same length"
else pure $ sum $ zipWith (*) (Prelude.reverse $ scanl1 (*) (init $ 1 : Prelude.reverse ns)) cs
decode' :: MonadError Error m => Array -> Array -> m Array
decode' = (\ns cs -> do
let err = DomainError "Decode arguments must be number arrays"
cs' <- asVector err cs >>= mapM (asNumber err)
ns' <- case asScalar err ns of
Right x -> Prelude.replicate (length cs') <$> asNumber err x
Left _ -> asVector err ns >>= mapM (asNumber err)
scalar . Number <$> decode ns' cs') `atRank2` (1, 1)
decodeBase2 :: MonadError Error m => Array -> m Array
decodeBase2 = decode' (scalar $ Number 2)
Encode
Encode and Base 2 Encode ⊤
are, in some sense, the inverse function to Decode. Encoding where the left argument is a vector does the opposite operation of decoding with that mixed base.
encode :: MonadError Error m => [Complex Double] -> Complex Double -> m [Complex Double]
encode [] _ = pure []
encode (bs :> b) n = do
let rem = if b == 0 then n else b `complexRemainder` n
let div = if b == 0 then 0 else complexFloor $ n / b
(`snoc` rem) <$> encode bs div
Encode where the left argument is a scalar is more complex. I'll quote the documentation:
If
x
[the left argument] is scalar, operates on vectors ofy
[the right argument],x
must have magnitude greater than1
and the following recursive procedure is performed:
- if
y=0
, thenr
is⟨⟩
- let
rem
bex|y
anddiv
be⌊y÷x
- let
rem1
bex|div
anddiv1
be⌊div÷x
- if
rem=rem1
and div=div1 thenr
isy
- otherwise
r
isrem⍪⍨x⊤div
In other words, the same number is used for the repeated modulo and floor-division until two results are equal, which means we have reached a point where we would loop.
encodeScalar :: MonadError Error m => Complex Double -> Complex Double -> m [Complex Double]
encodeScalar b _ | Cx.magnitude b <= 1 = throwError $ DomainError "Scalar encode left argument must be greater than 1 in magnitude"
encodeScalar _ n | Number n == Number 0 = pure []
encodeScalar b n = do
let rem = b `complexRemainder` n
let div = complexFloor $ n / b
let rem1 = b `complexRemainder` div
let div1 = complexFloor $ div / b
if rem == rem1 && div == div1 then pure [n]
else (`snoc` rem) <$> encodeScalar b div
encode' :: MonadError Error m => Array -> Array -> m Array
encode' = (\b n -> do
let err = DomainError "Encode arguments must be number arrays"
n' <- asScalar err n >>= asNumber err
case asScalar err b of
Right b' -> vector . fmap Number . (\xs -> if null xs then [0] else xs) <$> (asNumber err b' >>= flip encodeScalar n')
Left _ -> vector . fmap Number <$> (asVector err b >>= mapM (asNumber err) >>= flip encode n')) `atRank2` (1, 0)
encodeBase2 :: MonadError Error m => Array -> m Array
encodeBase2 = encode' (scalar $ Number 2)
Identity Modifiers
There are three "identity modifiers" that are taken from J and are very useful in modifier trains: Lev ⫣
, Dex ⊩
and Ident ⫤
. Respectively, these are a conjunction returning the left operand, a conjunction returning the right operand, an adverb returning the (only) operand.
ident = Adverb
{ adverbRepr = [G.ident]
, adverbContext = Nothing
, adverbOnArray = Just $ \n -> pure $ Function (Just $ F.constant1 n) (Just $ F.constant2 n) (makeAdverbRepr (show n) G.ident) Nothing
, adverbOnFunction = Just $ \f -> pure $ Function (Just $ callMonad f) (Just $ callDyad f) (makeAdverbRepr (show f) G.ident) Nothing }
lev = Conjunction
{ conjRepr = [G.lev]
, conjContext = Nothing
, conjOnArrayArray = Just $ \n m -> pure $ Function (Just $ F.constant1 n) (Just $ F.constant2 n) (makeConjRepr (show n) G.lev (show m)) Nothing
, conjOnArrayFunction = Just $ \n g -> pure $ Function (Just $ F.constant1 n) (Just $ F.constant2 n) (makeConjRepr (show n) G.lev (show g)) Nothing
, conjOnFunctionArray = Just $ \f m -> pure $ Function (Just $ callMonad f) (Just $ callDyad f) (makeConjRepr (show f) G.lev (show m)) Nothing
, conjOnFunctionFunction = Just $ \f g -> pure $ Function (Just $ callMonad f) (Just $ callDyad f) (makeConjRepr (show f) G.lev (show g)) Nothing }
dex = Conjunction
{ conjRepr = [G.dex]
, conjContext = Nothing
, conjOnArrayArray = Just $ \n m -> pure $ Function (Just $ F.constant1 m) (Just $ F.constant2 m) (makeConjRepr (show n) G.dex (show m)) Nothing
, conjOnArrayFunction = Just $ \n g -> pure $ Function (Just $ callMonad g) (Just $ callDyad g) (makeConjRepr (show n) G.dex (show g)) Nothing
, conjOnFunctionArray = Just $ \f m -> pure $ Function (Just $ F.constant1 m) (Just $ F.constant2 m) (makeConjRepr (show f) G.dex (show m)) Nothing
, conjOnFunctionFunction = Just $ \f g -> pure $ Function (Just $ callMonad g) (Just $ callDyad g) (makeConjRepr (show f) G.dex (show g)) Nothing }
Fold and Fold Back
Fold is like seeded Reduce, i.e. you pass an initial value.
fold :: MonadError Error m => (a -> a -> m a) -> a -> [a] -> m a
fold = foldlM
fold' :: MonadError Error m => (Array -> Array -> m Array) -> Array -> Array -> m Array
fold' f s xs = fold f s $ majorCells xs
foldBack :: MonadError Error m => (a -> a -> m a) -> a -> [a] -> m a
foldBack = foldrM
foldBack' :: MonadError Error m => (Array -> Array -> m Array) -> Array -> Array -> m Array
foldBack' f s xs = foldBack f s $ majorCells xs
Search Functions
Search functions all support "major cell search". This is breaking backwards compatibility with other APLs for Element Of. Thanks to @dzaima for helping me out with this definition.
searchFunction :: MonadError Error m => (Array -> [Array] -> m Array) -> Array -> Array -> m Array
searchFunction f ns hs = let cutRank = if arrayRank hs == 0 then 0 else arrayRank hs - 1
in if arrayRank ns < cutRank then throwError $ DomainError "Search function neelde must have rank at least equal to the rank of the major cells of the haystack"
else do
let hc = majorCells hs
nc <- atRank1 enclose' (toInteger cutRank) ns
onScalars1 (\n -> do
n' <- first n
f n' hc) nc
elementOf :: MonadError Error m => Array -> Array -> m Array
elementOf = searchFunction $ pure .: scalar .: boolToScalar .: elem
count :: MonadError Error m => Array -> Array -> m Array
count = searchFunction $ pure .: scalar .: Number .: (:+ 0) .: countEqual
indexOf :: MonadError Error m => Array -> Array -> m Array
indexOf = flip $ searchFunction $ pure .: scalar .: Number .: (:+ 0) .: (\n hs -> case n `genericElemIndex` hs of
Just x -> x + 1
Nothing -> genericLength hs + 1)
Interval Index
While Interval Index is also a search function, it's a bit more complex and might benefit from a description. The implementation makes pairs of lower and upper bounds for each section marked by the left argument and then finds the rightmost one where the right argument fits.
intervalIndex :: MonadError Error m => Array -> Array -> m Array
intervalIndex hs' ns =
if Prelude.not $ sorted $ majorCells hs' then throwError $ DomainError "Interval index left argument must be sorted"
else (searchFunction $ pure .: scalar .: Number .: (:+ 0) .: (\n hs -> do
let lowers = Nothing : fmap Just hs
let uppers = fmap Just hs :> Nothing
let bounds = Prelude.reverse $ zipWith3 (\l u i -> ((l, u), i)) lowers uppers [0..genericLength hs]
fromMaybe 0 $ fmap snd $ flip find bounds $ \case
((Just lower, Just upper), _) | lower <= n && n < upper -> True
((Just lower, Nothing), _) | lower <= n -> True
((Nothing, Just upper), _) | n < upper -> True
_ -> False)) ns hs'