Home

Awesome

Heftia: higher-order algebraic effects done right

Hackage Hackage Stackage LTS Stackage Nightly Build status

Heftia is an extensible effects library for Haskell that generalizes "Algebraic Effects and Handlers" to higher-order effects, providing users with maximum flexibility and delivering standard and reasonable speed. In its generalization, the focus is on ensuring predictable results based on simple, consistent semantics, while preserving soundness.

Please refer to the Haddock documentation for usage and semantics. For information on performance, please refer to performance.md.

This library is inspired by the paper:

The elaboration approach proposed in the above paper allows for a straightforward treatment of higher-order effects.

Heftia's data structure is an extension of the Freer monad, designed to be theoretically straightforward by eliminating ad-hoc elements.

Why choose this library over others?

This library is based on algebraic effects. Currently, none of the practical effect libraries other than this one are "algebraic." So, why is being algebraic important?

For example, algebraic effects are essential for managing coroutines, generators, streaming, concurrency, non-deterministic computations, and more in a highly elegant and concise manner.

Algebraic effects provide a consistent and predictable framework for handling side effects, enhancing modularity and flexibility in your code. Research in cutting-edge languages like Koka, Eff lang, and OCaml 5 is advancing the understanding and implementation of algebraic effects, establishing them as the programming paradigm of the future.

Heftia extends this by supporting higher-order algebraic effects, allowing for more expressive and modular effect management. This leads to more maintainable and extensible applications compared to non-algebraic effect libraries, positioning Heftia at the forefront of modern effect handling techniques.

Furthermore, Heftia is functionally a superset of other effect libraries, especially those based on ReaderT over IO. In other words, anything that is possible with other libraries is also possible with this library. This is because Heftia supports MonadUnliftIO in the form of higher-order effects.

Heftia should be a good substitute for mtl, polysemy, fused-effects, and freer-simple. Additionally, if performance is not a top priority, it should also be a good alternative for effectful. If performance is particularly important, effectful would be the best alternative to this library.

Key Features

Downsides

This library has notable semantic differences, particularly compared to libraries like effectful, polysemy, and fused-effects. The semantics of this library are almost equivalent to those of freer-simple and are also similar to Alexis King's eff library. This type of semantics is often referred to as continuation-based semantics. Additionally, unlike recent libraries such as effectful, which have an IO-fused effect system, the semantics of this library are separated from IO. Due to these differences, people who are already familiar with the semantics of other major libraries may find it challenging to transition to this library due to the mental model differences.

For those who have not used an extensible effects library in Haskell before, this should not be a problem. Particularly, if you are already somewhat familiar with the semantics of algebraic effects through languages like koka or eff-lang, you likely already have the mental model needed for this library, and everything should go smoothly.

Status

This library is currently in the beta stage. There may be significant changes and potential bugs.

I am looking forward to your feedback!

Getting Started

  1. $ cabal update
    
  2. Add heftia-effects ^>= 0.5 and ghc-typelits-knownnat ^>= 0.7 to the build dependencies. Enable the ghc-typelits-knownnat plugin, GHC2021, and the following language extensions as needed:

    • LambdaCase
    • DerivingStrategies
    • DataKinds
    • TypeFamilies
    • BlockArguments
    • FunctionalDependencies
    • RecordWildCards
    • DefaultSignatures
    • PatternSynonyms

Example .cabal:

...
    build-depends:
        ...
        heftia-effects ^>= 0.5,
        ghc-typelits-knownnat ^>= 0.7,

    default-language: GHC2021

    default-extensions:
        ...
        LambdaCase,
        DerivingStrategies,
        DataKinds,
        TypeFamilies,
        BlockArguments,
        FunctionalDependencies,
        RecordWildCards,
        DefaultSignatures,
        PatternSynonyms,
        TemplateHaskell,
        PartialTypeSignatures,
        AllowAmbiguousTypes
...

If you encounter an error like the following, add the pragma:

{-# OPTIONS_GHC -fplugin GHC.TypeLits.KnownNat.Solver #-}

to the header of your source file.

Could not deduce ‘GHC.TypeNats.KnownNat (1 GHC.TypeNats.+ ...)’

The supported versions are GHC 9.4.1 and later. This library has been tested with GHC 9.4.1, 9.6.6 and 9.8.2.

Example

Coroutine-based Composable Concurrent Stream

Below is an example of using concurrent streams (pipes).

{-# OPTIONS_GHC -fplugin GHC.TypeLits.KnownNat.Solver #-}

import Control.Monad.Hefty
import Control.Monad.Hefty.Concurrent.Stream
import Control.Monad.Hefty.Concurrent.Timer
import Control.Monad.Hefty.Except
import Control.Monad.Hefty.Unlift
import Control.Arrow ((>>>))
import Control.Monad (forever, void, when)
import Data.Foldable (for_)
import UnliftIO (bracket_)

-- | Generates a sequence of 1, 2, 3, 4 at 0.5-second intervals.
produce :: (Output Int <| ef, Timer <| ef) => Eff '[] ef ()
produce = void . runThrow @() $
    for_ [1 ..] \(i :: Int) -> do
        when (i == 5) $ throw ()
        output i
        sleep 0.5

-- | Receives the sequence at 0.5-second intervals and prints it.
consume :: (Input Int <| ef, Timer <| ef, IO <| ef) => Eff eh ef ()
consume = forever do
    liftIO . print =<< input @Int
    sleep 0.5

-- | Transforms by receiving the sequence as input at 0.5-second intervals,
--   adds 100, and outputs it.
plus100 :: (Input Int <| ef, Output Int <| ef, Timer <| ef, IO <| ef) => Eff eh ef ()
plus100 = forever do
    i <- input @Int
    let o = i + 100
    liftIO $ putStrLn $ "Transform " <> show i <> " to " <> show o
    output o
    sleep 0.5

main :: IO ()
main = runUnliftIO . runTimerIO $ do
    let produceWithBracket =
            bracket_
                (liftIO $ putStrLn "Start")
                (liftIO $ putStrLn "End")
                (raiseAllH produce)

    runMachineryIO_ $
        Unit @() @Int do
            produceWithBracket
            produceWithBracket
            >>> Unit @Int @Int plus100
            >>> Unit @Int @() consume
>>> main
Start
Transform 1 to 101
101
Transform 2 to 102
102
Transform 3 to 103
103
Transform 4 to 104
104
End
Start
Transform 1 to 101
101
Transform 2 to 102
102
Transform 3 to 103
103
Transform 4 to 104
104
End

The complete code example can be found at heftia-effects/Example/Stream/Main.hs.

Aggregating File Sizes Using Non-Deterministic Computation

The following is an extract of the main parts from an example of non-deterministic computation. For the full code, please refer to heftia-effects/Example/NonDet/Main.hs.

-- | Aggregate the sizes of all files under the given path
totalFileSize
    :: (Choose <| ef, Empty <| ef, FileSystem <| ef, Throw NotADir <| ef, IO <| ef)
    => FilePath
    -> Eff '[] ef (Sum Integer)
totalFileSize path = do
    entities :: [FilePath] <- listDirectory path & joinEither

    -- Non-deterministically *pick* one item from the list
    entity :: FilePath <- choice entities

    let path' = path </> entity

    liftIO $ putStrLn $ "Found " <> path'

    getFileSize path' >>= \case
        Right size -> do
            liftIO $ putStrLn $ " ... " <> show size <> " bytes"
            pure $ Sum size
        Left NotAFile -> do
            totalFileSize path'

main :: IO ()
main = runEff
    . runThrowIO @EntryNotFound
    . runThrowIO @NotADir
    . runDummyFS exampleRoot
    $ do
        total <- runNonDetMonoid pure (totalFileSize ".")
        liftIO $ print total

-- | Effect for file system operations
data FileSystem a where
    ListDirectory :: FilePath -> FileSystem (Either NotADir [FilePath])
    GetFileSize :: FilePath -> FileSystem (Either NotAFile Integer)

{- |
Interpreter for the FileSystem effect that virtualizes the file system in memory
based on a given FSTree, instead of performing actual IO.
-}
runDummyFS
    :: (Throw EntryNotFound <| ef, Throw NotADir <| ef)
    => FSTree
    -> Eff eh (FileSystem ': ef) ~> Eff eh ef
runDummyFS root = interpret \case
    ListDirectory path ->
        lookupFS path root <&> \case
            Dir entries -> Right $ Map.keys entries
            File _ -> Left NotADir
    GetFileSize path ->
        lookupFS path root <&> \case
            File size -> Right size
            Dir _ -> Left NotAFile
>>> main
Found ./README.md
 ... 4000 bytes
Found ./src
Found ./src/Bar.hs
 ... 1000 bytes
Found ./src/Foo.hs
 ... 2000 bytes
Found ./test
Found ./test/Baz.hs
 ... 3000 bytes
Sum {getSum = 10000}

Documentation

A detailed explanation of usage and semantics is available in Haddock. The example codes are located in the heftia-effects/Example/ directory. Also, the following HeftWorld example (outdated): https://github.com/sayo-hs/HeftWorld

About the internal elaboration mechanism: https://sayo-hs.github.io/jekyll/update/2024/09/04/how-the-heftia-extensible-effects-library-works.html

Comparison

Library or LanguageHigher-Order EffectsDelimited ContinuationEffect SystemPurely MonadicDynamic Effect RewritingSemantics
heftiaMulti-shotAlgebraic Effects
freer-simpleMulti-shotAlgebraic Effects
polysemyWeaving-based (functorial state)
effectful❌ (based on the IO monad)IO-fused
bluefin1❌ (based on the IO monad)2IO-fused
effMulti-shot❌ (based on the IO monad)Algebraic Effects & IO-fused 3
speffMulti-shot (restriction: 4)❌ (based on the IO monad)Algebraic Effects & IO-fused
in-other-wordsMulti-shot?❌?Carrier dependent
mtlMulti-shot (ContT)Carrier dependent
fused-effects❌?Carrier dependent & Weaving-based (functorial state)
Koka-langMulti-shot❌ (language built-in)Algebraic Effects
Eff-langMulti-shot❌ (language built-in)Algebraic Effects
OCaml-lang 5?One-shot5❌ (language built-in)?Algebraic Effects

Heftia can simply be described as a higher-order version of freer-simple. This is indeed true in terms of its internal mechanisms as well.

Additionally, this library provides a consistent algebraic effects semantics that is independent of carriers and effects. On the other hand, in libraries like in-other-words, mtl, and fused-effects, the semantics of the code depend on the effect and, in part, the carrier inferred by type inference. Fixing the semantics to a algebraic effects model helps improve the predictability of the behavior (interpretation result) of the code without losing flexibility.

Carrier-dependent semantics can lead to unexpected behavior for code readers, particularly in situations where the types become implicit. Particularly, attention should be given to the fact that due to type inference, semantic changes may propagate beyond the blocks enclosed by interpret or interpose. In the case of carrier-independent semantics, especially with Freer-based effects, interpret and interpose do not alter the semantics by intervening in type inference or instance resolution of the carrier. Instead, they function as traditional functions, simply transforming the content of the data structure. This results in minimal surprise to the mental model of the code reader.

Performance

Overall, the performance of this library is positioned roughly in the middle between the fast (effectful, eveff, etc.) and slow (polysemy, fused-effects, etc.) libraries, and can be considered average. In all benchmarks, the speed is nearly equivalent to freer-simple, only slightly slower.

For more details, please refer to performance.md.

Interoperability with other libraries

About mtl

About effectful

Representation of effects

Future Plans

License

The license is MPL 2.0. Please refer to the NOTICE. Additionally, the code from freer-simple has been modified and used internally within this library. Therefore, some modules are licensed under both MPL-2.0 AND BSD-3-Clause. For details on licenses and copyrights, please refer to the module's Haddock documentation.

Your contributions are welcome!

Please see CONTRIBUTING.md.

Acknowledgements, citations, and related work

The following is a non-exhaustive list of people and works that have had a significant impact, directly or indirectly, on Heftia’s design and implementation:

Footnotes

  1. https://discourse.haskell.org/t/what-is-a-higher-order-effect/10744

  2. https://discourse.haskell.org/t/bluefin-compared-to-effectful-video/10723/27?u=ymdfield

  3. https://github.com/hasura/eff/issues/12

  4. Scoped Resumption only. e.g. Coroutines are not supported.

  5. Effects do not appear in the type signature and can potentially cause unhandled errors at runtime