gelisam / klister

an implementation of stuck macros
BSD 3-Clause "New" or "Revised" License
131 stars 11 forks source link

Haskell FFI #165

Open gelisam opened 2 years ago

gelisam commented 2 years ago

Since Klister's type system is intentionally close to Haskell's, it would be great if we could call Haskell code from Klister.

One of the easiest-to-use FFI mechanisms I've seen is Haste's ffi function, which takes string containing javascript code and trusts the programmer to make sure the javascript expression does have type a:

ffi :: FFI r => String -> r

Where FFI a ensures that r has the form a -> b -> ... -> IO c, that those parameters have types which can be serialized to Javascript using some ToJS typeclass, and that the result type can be deserialized back to Haskell using some FromJS typeclass.

This nicely circumvents the difficulty that Haskell libraries aren't restricting themselves to the subset of Haskell types which Klister supports: the programmer can easily use Haskell's extra features inside the string, as long as it provides a Klister-compatible type at the boundary.

I am thinking of using the hint library (which I maintain) to evaluate a string of Haskell code, but there are some complications:

  1. A string is not sufficient: we must also specify the modules to import, the packages in which to look for those modules, and the package database(s) in which to look for those packages. A .ghc.environment.* file provides the latter two, and hint will look for it in the current-working-directory when klister runs.
  2. We could serialize to Strings and use the Read and Show instances, but that would be inefficient. The alternative is to use types which have Typeable instances, whose Dict would need to be provided by FromHaskell and ToHaskell typeclasses in Klister. Except, of course, Klister doesn't have typeclasses yet.
  3. While it is obvious how to serialize Klister integers, and relatively obvious how to serialize Klister strings (Haskell String? Haskell Text? Strict or Lazy?), it is a lot less obvious how to serialize Klister algebraic datatypes. We could define Haskell datatypes with the same names as the Klister datatypes, but Klister and Haskell have different naming conventions, so those names would need to be mangled. Or we could provide them as opaque values, plus some builtin functions for e.g. obtaining the constructor name or extracting a constructor's first argument as another opaque value.
  4. Sooner or later those strings containing Haskell code will grow too large, and the programmer will want to write helper functions and datatypes. Should we provide another function, ffi_decl : String -> IO (), for adding those definitions to some kind of environment which future ffi calls will see? How do multiple ffi calls making use of the same definitions make sure that the ffi_decl command they depend on is only run once? Perhaps the easiest would be to require the programmer to define a new package and to import their helper functions from there.
gelisam commented 1 year ago

Another challenge is that while a piece of Haskell code can use hint to evaluate a string to a value of any Typeable type, there will be a single piece of Haskell code which will be responsible for interpreting all the Haskell code found in all the Klister files, each of which has a different Klister type and a corresponding Haskell type.

One solution might be to use polymorphic recursion to instantiate that single piece of Haskell code at just the right type, but @david-christiansen came up with a simpler solution: define a single type, a GADT, capable of encoding a variety of Klister types: function of arbitrary arity over our few primitive types. It is less clear how to deal with datatypes.

data KlisterTypeRep a where
  KlisterInteger :: KlisterTypeRep Integer
  KlisterString :: KlisterTypeRep String
  KlisterArrow :: KlisterTypeRep a -> KlisterTypeRep b -> KlisterTypeRep (a -> b)

Then, using more GADTs and a Parameterized.Pair, we can construct e.g. a value of that type, or a TypeRep for that type, etc.

gelisam commented 1 year ago

It would also be quite useful to add an opaque ffi type to Klister, so that we can define e.g.

(define openFileForReading
  (the (-> String (IO (Opaque Handle)))
       (ffi "\\filePath -> openFile filePath ReadMode")))
(define hClose
  (the (-> (Opaque Handle) (IO Unit))))

without having to specify how a Handle is represented as an algebraic datatype on the Klister side.