For along time I thought about adding object-oriented features to PyFL but couldn’t figure out how. Classes and methods don’t seem to fit into the declarative model, where all objects are immutable. But then I stumbled on an answer almost by accident.
My lucky break came when I figured out how to add structs to PyFl.
Structs
Almost every other programming language has some form of struct. A struct is a record with named components. For example, in most languages you can declare something like
struct Person {
name: string
surname: string
dob: date
job: occupation
}
(here date and occupation are also structs, declared elsewhere).
Eliminating Mandatory Type Declarations
We could copy this approach but it involves mandatory type declarations, and those of the worst kind, namely those which contribute to the semantics of the program. In other words, they can’t be omitted, because the program doesn’t make sense without them.
I’ve always disliked mandatory type declarations. They’re a headache for the programmer who has to come up with them before writing any code, and they’re a headache for the implementer (in the case of PyFL, me) who has to parse and store them and extend the evaluator to deal with them.
I got this far with PyFL not requiring mandatory type declarations and I was determined not to introduce them. Yet basically every other language with structs requires some form of declaration. What was I to do?
What I did was take inspiration from PyFL’s treatment of lists. PyFL has lists but no list declarations. Instead it has various operations that produce lists, such as cons and append. Also it has list constants like [dog 3 'dick']. The lack of declarations has one obvious advantage, namely it allows heterogeneous lists, one (like the constant just given) that has elements of different types.
Adopting this approach to structs means specifying struct constants and operations which produce structs – an algebra of records.
Struct Constants
We begin with the constants. A struct is like a set, with named components. To specify a struct, we need to specify the components and their names. The components will be existing data objects, like lists, and we use the already existing notation for them. I chose {$ and $} to delimit the struct constants resulting, of example in,
{$ xcoord:3 ycoord:4 angle:0.785 $}
which denotes a structure with three components.
If a struct component can be any data object, why not another struct? For example:
{$ name:'Karen' surname:'Wilson'
dob:{$ year:1975 month:june day:28 $}
job:teacher
$}
Here june and teacher are word constants, but don’t have to be double quoted because they are appearing in a struct constant.
Struct Expressions
Now the question is, what are the struct operations or, more generally, the expressions that evaluate to a structure?
After a lot of thought I realized that you need an expression – not a constant – that evaluates to a struct. This construct expects expressions for the components and places their values in the structure, labelled by the given component names. I chose << and >> to delimit the construct, so, for example,
<< xcoord:3 ycoord: 3+1 angle: pi/4 >>
This is not a constant; the expressions 3+1 and pi/4 will be evaluated.
As with constants, structs can contain structs; e.g.
<< name: 'Karen' surname: 'Wilson'
dob: << year:2025-50 month:mon(6) day:4*7 >>
job: "teacher"
>>
What other operations do we need? One obvious one is access, returning the value of a component. This operation is almost universally denoted by “.” so I followed suit. If K is the struct denoted by the above expression then K.name is ‘Karen’ and K.dob.day is 28.
Finally there is a not so obvious function for combining structs. I called it xby for “extended by”. Given structs S and T, S xby T is T with missing values inherited from S. More precisely, the set of fieldnames of S xby T is the union of the sets of fieldnames of S and T; and given any fieldname n, the value of S xby T at n is that of T at n if T has n as a fieldname, or that of S at n otherwise.
Function Components
The next innovation is due to my friend Michael Levy, who suggested allowing function definitions inside structs. At first I didn’t understand, but eventually got the idea. It means having function components, and most of the languages with structs allow it. For example in
<<
xcoord:3
ycoord:4
degrees: lambda (r) 360*r/(2*pi) end
>>
the degrees component is a function (which converts radians to degrees). Then if we write
f(1.57)
where
f = C.degrees
C =
<<
xcoord:3
ycoord:4
degrees: lambda (r) 360*r/(2*pi) end
>>
end
we get 90 as the result.
I simplified the syntax so we can write C.degrees(1.570).
Self
However function components are not very interesting if they can’t refer to other components of the same structure. So I added a variable self that refers to the current structure. We can write
<< xcoord:3 ycoord:4 dist(): sqrt(self.xcoord**2+self.ycoord**2) >>
and if D is the above struct, D.dist() evaluates to 5.
These simple features work brilliantly together. For example, self xby << xcoord:6 >> is self with the xcoord changed to 6. Using this idea here is a more elaborate structure E
<<
xcoord:0
ycoord:0
left(dx): self xby << xcoord: self.xcoord+dx >>
up(dy): self xby << ycoord: self.ycoord+dy >>
dist(): sqrt(self.xcoord**2+self.ycoord**2)
>>
Then E.up(y) is E with the y coordinate set to y (overriding the current value). Similarly, F.left(x) is F with the x coordinate set to x. As a result E.up(4).left(3).dist() evaluates to 5.
OO?
I claim that the combination of features gives a reasonable analog of OO as found in imperative languages.
Structs are analogous to objects. Struct components with data values, like xcoord and ycoord, correspond to class variables. Struct components with function values correspond to methods.
Calling function values looks exactly like invoking methods, e.g. E.up(4). The main difference is that since structs (and all other PyFL data objects) are immutable, E is unchanged. But E.up(4) is the updated resulting structure, so we can chain the method calls as in E.up(40).left(3).dist().
Finally, inheritance. The xby operator provides the analog of inheritance. If D is a struct with default values for persons (e.g.country:Canada) then D xby P is the struct (object) P with P inheriting the defaults of D. But, as in real OO, P can override the defaults.
Well I’m happy with functional OO, especially without mandatory type declarations. How about you? Let me know.