cfg-0.0.2.0: src/KeyTree.hs
-- |
-- Module : KeyTree
-- Copyright : © Jonathan Lorimer, 2023
-- License : MIT
-- Maintainer : jonathanlorimer@pm.me
-- Stability : stable
--
-- @since 0.0.2.0
--
-- This module contains types for our internal tree representation of types and
-- configurations. It also contains some helper functions for working with
-- these trees. This should make it easier to implement different source
-- providers.
module KeyTree
( -- * Types
KeyTree
, KeyForest
-- * Helper Functions
, foldKeyTree
, appendFold
, mayAppendFold
, appendTraverse
, mayAppendTraverse
-- * Type Re-exports
, Map
, Free (..)
)
where
import Control.Monad.Free
import Data.Functor ((<&>))
import Data.Map.Strict
-- | Type alias for our internal tree structure. If this was written directly
-- as a sum type it would look like this:
--
-- @Pure value | Free (Map key (KeyTree key value))@
--
-- @since 0.0.2.0
type KeyTree key value = Free (Map key) value
-- | Type alias for a subtree
--
-- @since 0.0.2.0
type KeyForest key value = Map key (Free (Map key) value)
-- | Right fold on a 'KeyTree'. Uses 'Data.Map.foldrWithKey' under the hood.
--
-- @since 0.0.2.0
foldKeyTree
:: (Eq k, Eq v)
=> (v -> a)
-- ^ Function to run on 'Pure' values
-> (k -> Free (Map k) v -> a -> a)
-- ^ Step function for fold
-> a
-- ^ Initial accumulator
-> KeyTree k v
-- ^ KeyTree to fold
-> a
foldKeyTree _ stepF acc (Free m) = foldrWithKey stepF acc m
foldKeyTree valF _ _ (Pure val) = valF val
-- | A fold that appends a value at the leaf of the 'KeyTree', identifies what to
-- insert at the leaf by running a function on an accumulated value.
--
-- @since 0.0.2.0
appendFold
:: (Eq k, Eq v)
=> (a -> v -> v')
-- ^ Function to run on existing 'Pure' leaves @a@ is the accumulator @v@ is the value in @Pure@.
-> (a -> v')
-- ^ Function from accumulator @a@ to a value @v@. This happens when an empty subtree is found.
-> (k -> a -> Map k (Free (Map k) v) -> a)
-- ^ Step function for fold with key @k@ as an argument.
-> a
-- ^ Accumulator.
-> KeyTree k v
-- ^ KeyTree to append values to.
-> KeyTree k v'
appendFold valF accF stepF acc (Free m) =
if m == empty
then Pure $ accF acc
else Free $ mapWithKey (\k a -> appendFold valF accF stepF (stepF k acc m) a) m
appendFold valF _ _ acc (Pure v) = Pure $ valF acc v
-- | A fold that appends a value at the leaf of the 'KeyTree' (like
-- 'appendFold'), but the function on the accumulator can return a 'Maybe'. In
-- the @Nothing@ case we just append a 'Free' full of an empty 'Data.Map.Map'.
--
-- @since 0.0.2.0
mayAppendFold
:: (Eq k, Eq v)
=> (a -> v -> Maybe v')
-- ^ Function to run on existing 'Pure' leaves.
-> (a -> Maybe v')
-- ^ Function to run when an empty node is found.
-> (k -> a -> Map k (Free (Map k) v) -> a)
-- ^ Step function for fold with key @k@ as an argument.
-> a
-- ^ Accumulator.
-> KeyTree k v
-- ^ Tree to be folded.
-> KeyTree k v'
mayAppendFold valF accF stepF acc (Free m) =
if m == empty
then case accF acc of
Nothing -> Free empty
Just v -> Pure v
else Free $ mapWithKey (\k a -> mayAppendFold valF accF stepF (stepF k acc m) a) m
mayAppendFold valF _ _ acc (Pure v) =
case valF acc v of
Nothing -> Free empty
Just v' -> Pure v'
-- | Like 'appendFold' but with functions that return a value wrapped in an
-- 'Applicative' effect @f@. The function is suffixed with \"Traverse\" because
-- we use the 'sequenceA' to wrap the entire tree in a single 'Applicative'
-- effect.
--
-- @since 0.0.2.0
appendTraverse
:: (Applicative f, Eq k, Eq v)
=> (a -> v -> f v')
-- ^ Function to run on existing 'Pure' leaves.
-> (a -> f v')
-- ^ Function to run when an empty node is found.
-> (k -> a -> Map k (Free (Map k) v) -> a)
-- ^ Step function for fold with key @k@ as an argument.
-> a
-- ^ Accumulator.
-> KeyTree k v
-- ^ Tree to be folded.
-> f (KeyTree k v')
appendTraverse valF accF stepF acc = sequenceA . appendFold valF accF stepF acc
-- | Similar to 'appendTraverse' except the 'Applicative' effect can optionally
-- return a result. The function is manually implemented rather than using
-- 'Traversable' methods so that we don't have to require 'Traversable' on @f@.
-- This allows consumers to use effects like 'IO' that don't have a
-- 'Traversable' instance.
--
-- @since 0.0.2.0
mayAppendTraverse
:: (Applicative f, Eq k, Eq v)
=> (a -> v -> f v')
-> (a -> f (Maybe v'))
-> (k -> a -> Map k (Free (Map k) v) -> a)
-> a
-> KeyTree k v
-> f (KeyTree k v')
mayAppendTraverse valF accF stepF acc (Free m) =
if m == empty
then
accF acc <&> \case
Nothing -> Free empty
Just v -> Pure v
else Free <$> traverseWithKey (\k v -> mayAppendTraverse valF accF stepF (stepF k acc m) v) m
mayAppendTraverse valF _ _ acc (Pure v) = Pure <$> valF acc v