Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Model inheritance using functional programming style data types

I've been using F# recently and tried to code in a functional way rather than doing OOP all over again in a different syntax. I've now run into a problem that I could solve with a mix of inheritance and discriminated unions but for which I'm trying to find a pure functional style representation.

What I want to model is something like this (changed to preserve the pattern since I can't use the actual code):

type Shape =
    | Rectangle of Size * Size
    | Circle of Diameter

so far so good, but now I need to represent a collection of additional properties relevant for the different types of shapes, like:

type ShapeProperty =
    | Color of Shape * Color // Fine, valid for all shapes
    | Rotation of Shape * Angle // Wants to be Rotation of Rectangle * Angle
    | Details of Shape * int // Wants to be Detail of Circle * int

If instead of using a discriminated union for Shape, I'd used a base class and inheritance, I could refer to the actual types and make sure Rotation could only be applied to Rectangle and not Circle, but now I can't. Is there a way of implementing something like that while still keeping to pure functional data structures?

Edit:

My current solution is to separate the definition of a individual shapes from the fact that shapes are at all related, like this:

type Rectangle = Rectangle of Size * Size // Or using a record type
type Circle = Circle of Diameter // Or using a record type
type Shape = RectangleShape of Rectangle | CircleShape of Circle

which means that I then have type to refer to in ShapeProperty:

type ShapeProperty =
    | Color of Shape * Color
    | Rotation of Rectangle * Angle
    | Details of Circle * int

This feels a bit clumsy as now need to encapsulate each shape in the Shape type to store them in a collection, but it does give me a way of expressing the type safety I'm after. Any improvements on this would be welcome.

like image 720
SoftMemes Avatar asked Jan 22 '23 11:01

SoftMemes


1 Answers

I think discriminated unions may simply be the wrong tool for parts of this job. Just because you are writing functional code doesn't mean you should throw out everything you know.

Discriminated unions are (in some ways, and there is kind of a caveat to this) the inverse of inheritance(derived types), we could call them composed types.

In derived type models everything about the parent is guaranteed to be true about the children(LSP), in this composed type model everything about the children is guaranteed to be true about the parent.(i.e. instead of "new dog : animal, new cat : animal, it's new Dog, Cat : CatDog, so you get a new construction of all statements that are true about cats AND dogs. Think of it as the intersection of all statements about both cat and dog.)

So the question then becomes, "how do we work with derived types in OCaml?" Well, the "O" in there does stand for object...

I would suggest you create an object model with derived types for your shape heiarchy, as you have a top type(shape) and a bottom type(the empty shape,or null). And then you compose your ADT's out of those objects, so you have

abstract Shape , Circle : Shape, Rectangle : Shape,

Then you have

type ShapeProperty = | Foo of Shape | Bar of Rectangle | Blah of Circle;;

Anything that knows about a shape property has to be able to handle all three of those cases(reasonable since that's what you have it for), and circle and rectangle are assignment compatible to shape, so you don't have to worry about boxing conventions.

For what it's worth, YMMV etc. Haskell (the other major ML dialect running around) doesn't use objects for derived types, it uses what are known as "type classes" which are collections of properties/behaviors that you "derive from" and "guarantee an implementation of" thereby allowing a function to take in a concrete instance of a type class.

While much more functional in it's philosophy, trying to figure out what's going on gives me a headache since the conventional abstractions I use for object like constructions no longer apply, and I have to use these constructions that are logically equivalent, but semantically substantially more unusual(although I know a lot of people who use them to great success, so it might just be my own failings).

Note on the caveat, since tagged unions are tagged, all you have to do is guarantee a semantic action for all tags. That action could be "failwith 'why are you giving me one of those, I have no idea what to do with it'". The problem as you noticed with this construction is that you lose a lot of the really cool aspects of type safety in doing so. For example, you write a function that takes in an option int, and then if the option is nothing you throw an exception, well, why did you say you knew how to handle an option int?

like image 150
Snark Avatar answered Jan 30 '23 04:01

Snark