Search code examples
pact-lang

How do Pact interfaces provide abstraction similar to Haskell type classes?


The Pact documentation mentions that interfaces in Pact allow for abstraction similar to Haskell's type classes:

When a module issues an implements, then that module is said to ‘implement’ said interface, and must provide an implementation . This allows for abstraction in a similar sense to Java’s interfaces, Scala’s traits, Haskell’s typeclasses or OCaML’s signatures. Multiple interfaces may be implemented in a given module, allowing for an expressive layering of behaviors.

I understand that you can write an interface (this corresponds with declaring a class in Haskell), and then you can implement the interface for one or more modules (this corresponds with an instance in Haskell). For example:

-- This corresponds with declaring an interface
class Show a where
  show :: a -> String

-- This corresponds with implementing an interface
instance Show Bool where
  show True = "True"
  show False = "False"

However, the great value of a Haskell type class is that you can then abstract over it. You can write a function that takes any value so long as it is an instance of Show:

myFunction :: (Show a) => a -> ...
myFunction = ...

What is the corresponding concept in Pact? Is there a way to accept any module as a dependency, so long as it implements the interface? If not, how does this open up abstraction "in a similar sense to Haskell's type classes"?


Solution

  • I think your question may be conflating typeclasses with type variables and universal quantification. Typeclasses give you a common interface like show that can be used on any type (or in this case, module) that supports them. Universal quantification lets you write generic algorithms that work for any Show instance.

    Pact provides the former, but not the latter. The main utility is in giving your module a template to work against, and anyone who knows the interface will be able to use your module. This makes "swapping implementations" possible, but doesn't open the door to "generic algorithms". For that we'd need some way of saying "For all modules that implement interface"...

    UPDATE: As per Stuart Popejoy's comment, this sort of abstraction can indeed be achieved using modrefs. Here is an example of a module that implements a generic method over any module implementing a certain interface:

    (interface iface
      (defun op:integer (arg:string)))
    
    (module impl1 G
      (defcap G () true)
      (implements iface)
      (defun op:integer (arg:string) (length arg)))
    
    (module impl2 G
      (defcap G () true)
      (implements iface)
      (defun op:integer (arg:string) -1))
    
    (module app G
      (defcap G () true)
      (defun go:integer (m:module{iface} arg:string)
        (m::op arg)))
    
    (expect "impl1"  5 (app.go impl1 "hello"))
    (expect "impl2" -1 (app.go impl2 "hello"))