static-0.1.0.0: static.cabal
Cabal-Version: 3.0
Name: static
Version: 0.1.0.0
Synopsis: Type-safe and interoperable static values and closures
Description:
Serialise closures in a type-safe way that interoperates across binaries.
This package is inspired by @distributed-static@ and GHC's /static pointers/
in @GHC.StaticPtr@, which came out of the same research. However, we make
some significantly-different design choices, described below.
GHC made the design choice to focus on guaranteeing that static values could
be passed between nodes if they were running the exact same binary, since
they are indexed by 64-bit integers automatically-generated by the compiler.
@distributed-static@ attempts to support the same source program compiled by
different versions of GHC. As part of this effort to preserve stability, one
must pass in a table ('RemoteTable') whose keys represent the stability, and
whose values are resolved potentially differently across compiler versions.
The need for the caller to pass in a 'RemoteTable' is seen as a liability, so
two subsequent packages @distributed-closure@ and @static-closure@ take the
opposite approach, ripping out the 'RemoteTable' but doubling down on GHC's
choice to guarantee compatibility only across different processes running the
exact same binary program. Their uses cases are focused around compute
clusters and other forms of centralised distributed computing, where this is
easy to achieve and not a problem.
Sometimes security is cited as a reason to have this restriction, but this is
a /bogus argument/. "Guarantee compatibility only across same binary" means
"same binary => compatibility" whereas the bogus security argument depends on
"compatibility => same binary", which is not true - anyone who analyses your
binary will know which numbers to spoof, to convince your program via this
interface that they are "running the same code". Guaranteeing "same binary"
in an adversial setting is in fact extremely hard and cannot be achieved
perfectly; in the real-world it can only be approximated, and should be done
so via mechanisms designed for it, not via numbers that are slightly hard to
brute force at best and trivial to find out at worst.
This package makes the /opposite/ choice, intended for less restricted and
more open distributed computing environments such as the internet and
decentralised protocols. In these contexts, the requirement of running the
exact same binary program is impossible to achieve in practise. Furthermore,
we see it as an /advantage/ that code does not need to be exactly the same -
for example, one can serialise a closure and its inputs, /upgrade your code/,
then resume running the closure on the same deserialised input arguments but
with a bugfixed closure. The necessity to pass in an explicit 'RemoteTable'
(here simply called 'staticTab') is not a liability, but a useful tool to
represent high-level compatibility and interoperability. Two nodes with the
same keys in their 'staticTab's, know that they can talk to each other
interoperably even if their implementations differ significantly. One node
that wishes to talk to different nodes running different minor versions of
the same protocol, could instantiate two different 'staticTab's with the same
keys but different implementations, to handle behavioural nuances between the
minor versions. In general, it's a useful bit of metadata to keep around in
your program code, and can help you perform smooth upgrades of a
non-centralised networking protocol more easily.
There are also a few technical differences between this and
@distributed-static@, some of which could be re-adopted there too:
* We use dependent-types and type-level programming to guarantee type
safety, rather than 'Rank1Dynamic'. This enables us to store all possible
closure types instead of just rank-1 polymorphic functions, including but
not limited to: rank-n functions, functions with constraints, those using
higher-kinded types, etc.
* Our serialisation typeclasses are designed to interoperate with many
different serialisation frameworks. Instances for 'Data.Binary' and
'Codec.Serialise' are provided here for convenience.
* We have additional Template Haskell splices that support creating static
values from top-level definitions that must refer to other static values,
whether they be recursive or mutually-recursive or neither. This is
achieved using an implementation of @mfix@ for the @Q@ monad.
We did /not/ implement the ability to compose static references. The main
reason is that, in our view, the /purpose/ of static closures is to represent
which top-level tasks to execute, and the inputs to execute it on. This is
the /interface/ or /contract/ of this concept. /How you run/ the task is an
implementation detail, and as discussed above, this might be different across
different machines or as time passes and we upgrade the code. Therefore it
makes no sense to serialise a representation of "task A is the composition of
closure B and closure C", because it is irrelevant to the /interface/.
If your /interface/ is actually "run arbitrary user-defined code" (e.g. in a
VM or EDSL evaluator) then it would indeed make sense to support composition,
but then you should define your own AST, evaluator, and serialiser for this;
and pass the ASTs around as regular runtime values, not static values.
Supporting arbitrary ASTs like this is outside of the scope of this library.
Further, in Haskell there are many ways of applying values not just @(_ :: a
-> b) (_ :: a) :: b@, e.g. with constraints, with a combination of static and
non-static arguments, with type applications, and so on. Only the simple form
@(_ :: a -> b) (_ :: a) :: b@ is likely to be interoperable across multiple
languages. Supporting AST statics would therefore unnecessarily restrict how
we can implement the behaviour of a static closure in our chosen language.
See unit tests for example usage, e.g.
<https://github.com/infinity0/hs-static/tree/master/test/Control/Static/UnitTests.hs UnitTests>
Homepage: https://github.com/infinity0/hs-static
Bug-Reports: https://github.com/infinity0/hs-static/issues
License: GPL-3.0-or-later
License-File: LICENSE.GPL-3
Author: Ximin Luo
Maintainer: infinity0@pwned.gg
Copyright: 2020 Ximin Luo
Category: Control, Static, Closure
Tested-With: GHC >= 8.8.3
Extra-Source-Files: CHANGELOG.md
Source-Repository head
Type: git
Location: https://github.com/infinity0/hs-static
Flag dev
Description: Set compile flags for development
Default: False
Manual: True
Common generic
Default-Language: Haskell2010
Build-Depends: base >= 4 && < 5,
GHC-Options:
-Wall
-Wno-unused-matches
-Wredundant-constraints
-Wincomplete-record-updates
-Wincomplete-uni-patterns
if flag(dev)
GHC-Options:
-Werror
-O2
-- some optimisations cause memory leaks; switch on -O2 and profiling so we
-- can detect this during development so it doesn't cause surprises later
GHC-Prof-Options:
-fprof-auto
Library
Import: generic
Build-Depends:
constraints
, text
, singletons
-- for serialisation
, binary
, bytestring
, serialise
-- Control.Static.TH
, template-haskell
HS-Source-Dirs: src
Exposed-Modules:
Control.Static
, Control.Static.Common
, Control.Static.Closure
, Control.Static.Serialise
, Control.Static.Static
, Control.Static.TH
Test-Suite doctests
Import: generic
GHC-Options: -threaded
Build-Depends:
doctest
, static
HS-Source-Dirs: test
Type: exitcode-stdio-1.0
Main-Is: DocTests.hs
Test-Suite unit
Import: generic
GHC-Options: -threaded
Build-Depends:
tasty
, tasty-quickcheck
, tasty-hunit
, checkers
, static
, lens
, mtl
, transformers
HS-Source-Dirs: test
Type: exitcode-stdio-1.0
Main-Is: UnitTests.hs
Other-Modules:
Control.Static.UnitTests