Re: [Haskell-cafe] more thoughts on "Finally tagless"

On Tue, Mar 9, 2010 at 2:45 PM, Tillmann Rendel
Tom Schrijvers wrote:
Yeah, subject "Finally Tagless" again, sorry, I'm just not done with it yet.
In Olegs haskell implementation he is using classes mainly to model the syntax and instances to use for evaluators / compilers to allow multiple interpretations.
I wonder if it'd be possible to do the same without classes using data types instead?
Something similar to what Luke Palmer has done here:
http://lukepalmer.wordpress.com/2010/01/24/haskell-antipattern-existential-t...
You can just use the dictionary translation of type classes to replace them with data types:
E.g.
class Eval sem where val :: Int -> sem Int add :: sem Int -> sem Int -> sem Int
-->
data EvalDict sem = EvalDict { val :: Int -> sem Int, add :: sem Int -> sem Int -> sem Int }
This is the way to program in the final tagless style without using type classes, but there is an important difference to what Luke Palmer did in the blog post cited above. While both approaches allow to encode type classes using data types, they are technically different, and applicable for different programs.
In the dictionary approach, the dictionary contains fields of the same types as the members of the class. For example, val has the type Int -> sem Int in both the type class and the data type.
This is not the case in the pattern described by Luke. There, the w type is dropped from the types of the methods when converting to a data type. For example, the type of growHorizontal is w -> Bool in the type class, but simply Bool in the data type.
The reason for this difference is that Luke uses the fact that w is going to be always existentially bound, so it will not be externally usable anyway. The information contained in the w values can just as well be stored in each of the fields of the data type.
The dictionary approach encodes abstract data types: The data type is the interface, each value of the data type is an implementation, and a function which takes the dictionary and is parametric in the type parameters of the dictionary is a program which uses the data type abstractly.
For example, a program using the Eval abstract data type from above could look like this:
-- using the type class add_two :: Eval sem => sem -> sem add_two x = add x (val 2)
-- using the dictionary add_two :: EvalDict sem -> sem -> sem add_two dict x = add dict x (val dict 2)
On the other hand, Luke describes an encoding of objects: The data type describes the interface of an object, and each value of that data type is an object, with possibly different implementations of each function in the interface. A program using these objects can invent arbitrary new objects, as long as all fields in the interface are defined.
Since abstract data types and objects are fundamentally different, so are these two programming patterns.
Tillmann, Thanks for your insight of objects vs. abstract data types. William Cook's Onward! essay is relevant here. He characterizes the difference between objects and abstract data types nicely: the latter allow binary methods that pattern match (to exploit the combined knowledge of the internals of two different values) whereas objects only know their own implementation. Dictionaries by themselves are objects in Cook's sense: they are just a record of functions that cannot be inspected. We can have an infinite number of them (while we can only have one type class instance per sem type). However, the (sem a) values are not objects, and varying the dictionaries while keeping the sem type the same does not seem very useful for implementing different semantics. For a type class function like add :: sem Int -> sem Int -> sem Int, binary pattern matching seems essential for meaningful implementations, and hence objects don't make much sense. Would you agree? Cheers, Tom

Tom Schrijvers wrote:
William Cook's Onward! essay is relevant here. He characterizes the difference between objects and abstract data types nicely: the latter allow binary methods that pattern match (to exploit the combined knowledge of the internals of two different values) whereas objects only know their own implementation.
Dictionaries by themselves are objects in Cook's sense: they are just a record of functions that cannot be inspected. We can have an infinite number of them (while we can only have one type class instance per sem type).
I agree that dictionaries can be seen as objects. This is an interesting point of view. At first glance, dictionaries seem to be not that interesting objects, becaus the observation functions never return new objects, but only plain values instead. However, we can use the object-like nature of dictionaries to produce new dictionaries in creative ways. For example, we could produce a dictionary by performing the operations in two dictionaries in parallel: evalProduct eval1 eval2 = EvalDict valProduct addProduct where valProduct x = (val eval1 x, val eval2 x) addProduct (a, b) (c, d) = (add eval1 a c, add eval2 b d) Of course, the same can be done with typeclass using a newtype. In [1], we argue that this kind of code enables us to implement semantics of EDSLs as components, which can be composed etc. Since we used Scala, we had modelled dictionaries as objects. But with this point of view about dictionaries as objects, its the exact same story in Haskell, of course. [1] Hofer et al. Polymorphic embedding of DSLs. GPCE 2008.
For a type class function like add :: sem Int -> sem Int -> sem Int, binary pattern matching seems essential for meaningful implementations, and hence objects don't make much sense. Would you agree?
Well, we could encode numbers as objects using church numerals, similar to how Cook uses characteristic functions for sets: data Number = Number (iter :: forall a . (a -> a) -> a -> a) The val constructor: valNumber :: Int -> Number valNumber x = Number (\f -> iterate f !! x) Addition: add :: Number -> Number -> Number add (Number iter1) (Number iter2) = Number (\f -> iter1 f . iter2 f)
the (sem a) values are not objects, and varying the dictionaries while keeping the sem type the same does not seem very useful for implementing different semantics.
We could use the Number objects to implement sem Int as follows. (Luckily, sem was always applied to Int in this reduced example, so we do not have to introduce non-parametric type-level functions): newtype Const a b = Const a evalAsObject :: EvalDict (Const NumberObject) evalAsObject = EvalDict valAsObject addAsObject where valAsObject x = Const (valNumber x) addAsObject (Const a) (Const b) = Const (add a b) We can often (always?) provide a sufficiently rich interface to our objects to support the same operations as with an abstract data type. I am not sure what laziness does to the picture in Cook's essay. Could a thunk be seen as an object with force as the only observing function? That would mean that in Haskell, even algebraic data types behave like objects because we are not handling them directly, but rather their thunks. From this point of view, Haskell is purely object-oriented. Tillmann
participants (2)
-
Tillmann Rendel
-
Tom Schrijvers