commit 480b499ddd4fe78f1f4f4791a470cb00027b1e5f Author: VegOwOtenks Date: Fri May 2 22:27:45 2025 +0200 Implementation and Documentation diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3a5b475 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.stack-work/ diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..3c56f7e --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,11 @@ +# Changelog for `Infinitree` + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to the +[Haskell Package Versioning Policy](https://pvp.haskell.org/). + +## Unreleased + +## 0.1.0.0 - YYYY-MM-DD diff --git a/Infinitree.cabal b/Infinitree.cabal new file mode 100644 index 0000000..006471b --- /dev/null +++ b/Infinitree.cabal @@ -0,0 +1,36 @@ +cabal-version: 2.2 + +-- This file has been generated from package.yaml by hpack version 0.38.0. +-- +-- see: https://github.com/sol/hpack + +name: Infinitree +version: 0.1.0.0 +synopsis: Infinitely deep trees for lazy stateless memoization +description: Please see the README on GitHub at +author: VegOwOtenks +maintainer: vegowotenks@jossco.de +copyright: 2025 VegOwOtenks +license: AGPL-3.0-or-later +license-file: LICENSE +build-type: Simple +extra-source-files: + README.md + CHANGELOG.md + +library + exposed-modules: + Data.Infinitree + Data.Infinitree.Examples + other-modules: + Paths_Infinitree + autogen-modules: + Paths_Infinitree + hs-source-dirs: + src + ghc-options: -Wall -Wextra -Wcompat -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns -Wmissing-export-lists -Wmissing-home-modules -Wpartial-fields -Wredundant-constraints + build-depends: + adjunctions + , base >=4.7 && <5 + , distributive + default-language: Haskell2010 diff --git a/README.md b/README.md new file mode 100644 index 0000000..7ea9cf1 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +# Infinitree diff --git a/Setup.hs b/Setup.hs new file mode 100644 index 0000000..9a994af --- /dev/null +++ b/Setup.hs @@ -0,0 +1,2 @@ +import Distribution.Simple +main = defaultMain diff --git a/package.yaml b/package.yaml new file mode 100644 index 0000000..2957151 --- /dev/null +++ b/package.yaml @@ -0,0 +1,39 @@ +name: Infinitree +version: 0.1.0.0 +license: AGPL-3.0-or-later +author: "VegOwOtenks" +maintainer: "vegowotenks@jossco.de" +copyright: "2025 VegOwOtenks" + +extra-source-files: +- README.md +- CHANGELOG.md + +# Metadata used when publishing your package +synopsis: Infinitely deep trees for lazy stateless memoization +# category: Web + +# To avoid duplicated efforts in documentation and dealing with the +# complications of embedding Haddock markup inside cabal files, it is +# common to point users to the README.md file. +description: Please see the README on GitHub at + +dependencies: +- base >= 4.7 && < 5 +- adjunctions +- distributive + +ghc-options: +- -Wall +- -Wextra +- -Wcompat +- -Widentities +- -Wincomplete-record-updates +- -Wincomplete-uni-patterns +- -Wmissing-export-lists +- -Wmissing-home-modules +- -Wpartial-fields +- -Wredundant-constraints + +library: + source-dirs: src diff --git a/src/Data/Infinitree.hs b/src/Data/Infinitree.hs new file mode 100644 index 0000000..52ebfb2 --- /dev/null +++ b/src/Data/Infinitree.hs @@ -0,0 +1,182 @@ +{-# LANGUAGE DeriveFunctor #-} +{-# LANGUAGE InstanceSigs #-} +{-# LANGUAGE TypeFamilies #-} +{-# LANGUAGE TypeApplications #-} +{-# LANGUAGE BangPatterns #-} +{-# LANGUAGE DeriveFoldable #-} +{-# LANGUAGE RankNTypes #-} + +-- | +-- Copyright: (c) Luca S. Jaekel +-- License: AGPL3 +-- +-- Infinitrees are memoization trees, which can be used to avoid dealing with mutable caches. + +module Data.Infinitree +-- export the structure and the instances but not any accessors because you're not meant to invalidate the Infinitree +( Infinitree() +-- * Identity trees +, nats +, ints +, nums +-- * Construction Functions +, build +, buildInt +, buildNum +) +where + +-- Distributive is a typeclass which allows you to use the law of distribution on functors +-- It is a superclass constraint for Representable, which is why I have to define it +import Data.Distributive (Distributive (distribute)) + +-- Representable functors allow indexing and construction from indices +-- they have a index type (Rep :: Type -> Type) +-- index takes an index and a structure, returns the element +-- tabulate calls your function with every index to build the functor +import Data.Functor.Rep (Representable, Rep, tabulate, index) + +-- Natural numbers are [0..] +import Numeric.Natural (Natural) + +-- ternary operator of sorts +import Data.Bool (bool) + +-- | This tree is infinite, it doesn't end anywhere. +-- +-- You can index into it infitely. +-- +-- It always has a left and a right branch. Every Branch also holds a value. + +data Infinitree a = Branch + { left :: Infinitree a -- left branch, smaller number, the first left branch contains all odd numbers + , leaf :: a -- current number, 0 for all intents and purposes + , right :: Infinitree a -- right branch, bigger number, the first right branch contains all even numbers + } + +instance Functor Infinitree where + fmap :: (a -> b) -> Infinitree a -> Infinitree b + fmap f tree = Branch (fmap f (left tree)) (f $ leaf tree) (fmap f (right tree)) + +instance Applicative Infinitree where + pure :: a -> Infinitree a + pure e = Branch (pure e) e (pure e) + (<*>) :: Infinitree (a -> b) -> Infinitree a -> Infinitree b + (<*>) (Branch fl f fr) (Branch vl v vr) = Branch (fl <*> vl) (f v) (fr <*> vr) + liftA2 :: (a -> b -> c) -> Infinitree a -> Infinitree b -> Infinitree c + liftA2 f (Branch la va ra) (Branch lb vb rb) = Branch (liftA2 f la lb) (f va vb) (liftA2 f ra rb) + (*>) :: Infinitree a -> Infinitree b -> Infinitree b + (*>) = flip const + (<*) :: Infinitree a -> Infinitree b -> Infinitree a + (<*) = const + +-- >>> [1, 2] <* [1, 2] +-- [1,1,2,2] + +-- I could not define a useful Foldable instance +-- +-- instance Foldable Infinitree where +-- foldMap :: Monoid m => (a -> m) -> Infinitree a -> m +-- foldMap f (Branch l v r) = f v + +-- | This is a superclass constraint for representable, but it is entirely implementable from Representable +-- +-- I now learned that I could have derived it via the Co newtype from Data.Functor.Rep +-- +-- https://hackage-content.haskell.org/package/adjunctions-4.4.3/docs/Data-Functor-Rep.html#t:Co + +instance Distributive Infinitree where + distribute :: Functor f => f (Infinitree a) -> Infinitree (f a) + distribute f = tabulate (\ i -> fmap (flip index i) f) + +-- Representable allows indexing and construction +instance Representable Infinitree where + -- only natural numbers index into this structure + type Rep Infinitree = Natural + + tabulate :: (Rep Infinitree -> a) -> Infinitree a + tabulate f' = let + + -- build a tree structure of numbers, like this + -- _0 + -- _/ \_ + -- _/ \_ + -- _/ \_ + -- / \ + -- _1 2_ + -- / \ / \ + -- / \ / \ + -- / \ / \ + -- 3 5 4 6 + -- / \ / \ / \ / \ + -- 7 11 9 13 8 12 10 14 + tabulate' :: (Rep Infinitree -> a) -> Rep Infinitree -> Natural -> Infinitree a + tabulate' f !i !s = let -- keep the indices strict to avoid function application chains + l = i + s + r = l + s + s' = 2 * s + in Branch (tabulate' f l s') (f i) (tabulate' f r s') + + in tabulate' f' 0 1 + + -- index into the tree structure recursively + -- the current leaf always has value 0, the index will be adjusted along the way + index :: Infinitree a -> Rep Infinitree -> a + index t n = let + + -- inner recursive function + index' !tree !0 = leaf tree + index' !tree !i = index' subtree q + where + (!q, !r) = pred i `quotRem` 2 -- q is strict to avoid useless function application delays + !subtree = bool right left (r == 0) $ tree + + in index' t n + +-- * Identity trees +-- +-- These are probably not optimal for performance, since you always have two trees in memory if you `fmap` over them + + +-- | a tree of natural numbers. +-- +-- a use case would be to `fmap` over it to transform it. +-- +-- >>> map (index nats) [0..15] +-- [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15] + + +nats :: Infinitree Natural +nats = tabulate @Infinitree id + +-- | tree of integer numbers +-- +-- in case you don't want to transform to integer for mapping + +ints :: Infinitree Integer +ints = tabulate @Infinitree toInteger + +-- | a tree of generic numbers +-- +-- if you need a specific number type, make sure you don't use a bounded type, the tree is infinite + +nums :: Num n => Infinitree n +nums = tabulate @Infinitree fromIntegral + + +-- * Construction functions + +-- | build using the infinitree indices + +build :: (Natural -> a) -> Infinitree a +build = tabulate @Infinitree + +-- | build using arbitrary-width integers + +buildInt :: (Integer -> a) -> Infinitree a +buildInt = tabulate @Infinitree . (. toInteger) + +-- | build using whatever num type you need + +buildNum :: Num n => (n -> a) -> Infinitree a +buildNum = tabulate @Infinitree . (. fromIntegral) diff --git a/src/Data/Infinitree/Examples.hs b/src/Data/Infinitree/Examples.hs new file mode 100644 index 0000000..96a484f --- /dev/null +++ b/src/Data/Infinitree/Examples.hs @@ -0,0 +1,36 @@ +{-# LANGUAGE TypeApplications #-} +-- | +-- Copyright: (c) Luca S. Jaekel +-- License: AGPL3 +-- +-- This is the example usage module, you're meant to look at the source code, feel free to click the `Source` link below +module Data.Infinitree.Examples +(fib) +where + +import Data.Infinitree ( Infinitree ) + +import qualified Data.Functor.Rep as Representable +import Numeric.Natural (Natural) + +-- | This defines a convenience function +-- users wouldn't have to call Representable.index fibonacci themselves +-- This example is written to have you look at the source code for example usage. + +fib :: Natural -> Integer +fib = Representable.index fibonacci + +-- | a tree of all fibonacci numbers +-- +-- while this enables memoization it also adds a O(log n) overhead to every lookup + +fibonacci :: Infinitree Integer +fibonacci = Representable.tabulate @Infinitree go + -- `Representable.tabulate @Infinitree go` is equivalent to `fmap go nats` but more efficient because it doesn't maintain two trees + where + -- go is the fibonacci function, it will be called with every index + go 0 = 0 + go 1 = 1 + -- sum the lower to fibonacci numbers from the tree + go n = Representable.index fibonacci (n - 1) + Representable.index fibonacci (n - 2) + diff --git a/stack.yaml b/stack.yaml new file mode 100644 index 0000000..c00438b --- /dev/null +++ b/stack.yaml @@ -0,0 +1,66 @@ +# This file was automatically generated by 'stack init' +# +# Some commonly used options have been documented as comments in this file. +# For advanced use and comprehensive documentation of the format, please see: +# https://docs.haskellstack.org/en/stable/configure/yaml/ + +# A 'specific' Stackage snapshot or a compiler version. +# A snapshot resolver dictates the compiler version and the set of packages +# to be used for project dependencies. For example: +# +# snapshot: lts-23.0 +# snapshot: nightly-2024-12-13 +# snapshot: ghc-9.8.4 +# +# The location of a snapshot can be provided as a file or url. Stack assumes +# a snapshot provided as a file might change, whereas a url resource does not. +# +# snapshot: ./custom-snapshot.yaml +# snapshot: https://example.com/snapshots/2024-01-01.yaml +snapshot: nightly-2025-04-09 + +# User packages to be built. +# Various formats can be used as shown in the example below. +# +# packages: +# - some-directory +# - https://example.com/foo/bar/baz-0.0.2.tar.gz +# subdirs: +# - auto-update +# - wai +packages: +- . +# Dependency packages to be pulled from upstream that are not in the snapshot. +# These entries can reference officially published versions as well as +# forks / in-progress versions pinned to a git hash. For example: +# +# extra-deps: +# - acme-missiles-0.3 +# - git: https://github.com/commercialhaskell/stack.git +# commit: e7b331f14bcffb8367cd58fbfc8b40ec7642100a +# +# extra-deps: [] + +# Override default flag values for project packages and extra-deps +# flags: {} + +# Extra package databases containing global packages +# extra-package-dbs: [] + +# Control whether we use the GHC we find on the path +# system-ghc: true +# +# Require a specific version of Stack, using version ranges +# require-stack-version: -any # Default +# require-stack-version: ">=3.3" +# +# Override the architecture used by Stack, especially useful on Windows +# arch: i386 +# arch: x86_64 +# +# Extra directories used by Stack for building +# extra-include-dirs: [/path/to/dir] +# extra-lib-dirs: [/path/to/dir] +# +# Allow a newer minor version of GHC than the snapshot specifies +# compiler-check: newer-minor diff --git a/stack.yaml.lock b/stack.yaml.lock new file mode 100644 index 0000000..d28a9f4 --- /dev/null +++ b/stack.yaml.lock @@ -0,0 +1,12 @@ +# This file was autogenerated by Stack. +# You should not edit this file by hand. +# For more information, please see the documentation at: +# https://docs.haskellstack.org/en/stable/topics/lock_files + +packages: [] +snapshots: +- completed: + sha256: d129ed943d7204d72bbdf2c688d9c68030d901ee06a60223f52eae9c900ba976 + size: 677117 + url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/nightly/2025/4/9.yaml + original: nightly-2025-04-09