Hi David,
The answer is, honestly, it depends :) As a programmer, if you have any prior OO programming experience, the methods may feel more natural. As a teacher, it depends on your curriculum.
Most of the curricula that Pyret support (e.g. HtDP, DCIC, Bootstrap, etc) tend to take a functional-programming-first approach. In those contexts, obviously the functions will be preferable. To date, Pyret has more fully-fleshed-out libraries in the functional style than the method style, but we do aim for feature parity in those libraries, so that teachers can choose either approach as they prefer.
In my opinion, methods on data definitions are an intermediate pedagogic point, sitting between “purely functional programming” and “purely OO programming”. Methods in Pyret don’t only exist on data definitions; they can exist on object literals, too:
my-posn = {
x: 3,
y: 4,
method dist-to-O(self): num-sqrt(num-sqr(self.x) + num-sqr(self.y)) end
}
check:
my-posn.dist-to-O() is 5
end
If you then have some other functionality in your program that relies on “values that have a dist-to-O method”, then any object with such a method will suffice. This is basically the essence of interface-focused OOP, rather than class-focused OOP. So the pedagogic progression would go
cases
to distinguish the data variantssharing
clause of a data definition – this still uses cases
to distinguish the variants, but allows calling the
functionality as a method rather than a function.cases
expression. This introduces the concept of dynamic dispatch,
separately from any distractions about classes, interfaces,
other OO whatevers…As I said, most of the curricula we’ve developed don’t go through all these steps, and focus on #1, while our library idiomatically uses 1, 2 or 3. I do hope we figure out how to support #4, eventually, too!
Ben
I am new to Pyret and unclear when and why one would use methods versus functions with a data definition. Here is some sample code:
# Exploration of data methods and functions.
# data type defining a triple of values and its methods
data Triple:
| no-triple with:
method rotr(self): no-triple end,
method rotl(self): no-triple end,
method noop(self): no-triple end,
| triple(a, b, c) with:
method rotr(self): triple(self.c, self.a, self.b) end,
method rotl(self): triple(self.b, self.c, self.a) end,
method noop(self): triple(self.a, self.b, self.c) end,
where:
no-triple satisfies is-Triple
no-triple satisfies is-no-triple
triple('1', '2', '3') satisfies is-Triple
triple('1', '2', '3') satisfies is-triple
no-triple.rotr() is no-triple
no-triple.rotl() is no-triple
no-triple.noop() is no-triple
triple('1', '2', '3').rotr() is triple('3', '1', '2')
triple('1', '2', '3').rotl() is triple('2', '3', '1')
triple('1', '2', '3').noop() is triple('1', '2', '3')
end
fun rotr(trip):
doc: 'return trip rotated right'
cases (Triple) trip:
| no-triple => no-triple
| triple(x, y, z) => triple(z, x, y)
end
where:
no-triple is no-triple
rotr(triple(1, 2, 3)) is triple(3, 1, 2)
end
fun rotl(trip):
doc: 'return trip rotated left'
cases (Triple) trip:
| no-triple => no-triple
| triple(x, y, z) => triple(y, z, x)
end
where:
no-triple is no-triple
rotl(triple(1, 2, 3)) is triple(2, 3, 1)
end
fun noop(trip):
doc: 'return trip unchanged'
cases (Triple) trip:
| no-triple => no-triple
| triple(x, y, z) => triple(x, y, z)
end
where:
no-triple is no-triple
noop(triple(1, 2, 3)) is triple(1, 2, 3)
end
The methods / functions rotr, rotl, noop all have the same semantics. Which approach is preferable and why?
Thanks.
- dcp--
You received this message because you are subscribed to the Google Groups "Pyret Discuss" group.
To unsubscribe from this group and stop receiving emails from it, send an email to pyret-discus...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/pyret-discuss/b2906414-ca3a-4156-ba67-19e89bd71a10n%40googlegroups.com.
Your data definitions here, of Reflector, Triple, and Rotor, Notch, etc from your project, are all basically just enumerated data; they don’t have much behavior of their own. The main difference between enumerated data definitions and interface-oriented programming is an enumeration is known to be finite at compile time (e.g. “there are only three distinct Reflectors”) and you can exhaustively pattern-match against them (“is it option 1, or option 2, or option 3?”). By contrast, interfaces are open-ended: in theory, anyone can program a new object that complies with an interface, and any code that relies on that interface doesn’t care about what the specific implementation is.
So, to revise your specific examples a bit:
I would write your Reflector data definition as
data Reflector:
| ukw-a with:
method alpha(self): 'EJMZALYXVBWFCRQUONTSPIKHGD' end
| ukw-b with:
method alpha(self): 'YRUHQSLDPXNGOKMIEBFZCWVJAT' end
| ukw-c with:
method alpha(self): 'FVPJIAOYEDRZXWGCTKUQSBNMHL' end
sharing:
method name(self): string-to-upper(to-string(self)) end
end
Note that if the
Enigma operators had decided to introduce a fourth reflector –
your code would have to change this Reflector data definition,
and then anything that relied on cases(Reflector) some-reflector
would break. On the other hand, the rest of your code doesn’t
ever need to use cases(Reflector):
all you do is call .alpha()
on it. All you seem to need is “an object with a .alpha() method
and a .name() method”, and neither of those actually use their self
parameter, so all you really need is just “an object
with a .alpha field and a .name field”. And then ukw-a,
ukw-b
and ukw-c
would just be constants in your code, and if the Enigma
operators had created a new reflector, you’d just need to define
a fourth constant somewhere and no other code would need to
change.
Your Triple example is a bit broken, though: once you’ve rotated your triple, you can’t rotate the result again. So the result of rotation isn’t itself a Triple any more. If you wanted to write that one in object-oriented style, I’d write it as
fun triple(f, s, t):
{
first: f,
second: s,
third: t,
method rotl(self): triple(self.second, self.third, self.first) end,
method rotr(self): triple(self.third, self.first, self.second) end,
method noop(self): self end
}
end
no-triple = {
method rotl(self): self end,
method rotr(self): self end,
method noop(self): self end
}
I’m not sure what the
intent of the no-triple
part of the Triple example was, so I can’t refine this any
further.
Thank you for your response. I have been working on a project (https://github.com/psb-david-petty/enigma) and have used functions for most of the algorithms, but added data definitions for some of the constant (literal) data. That means that the
> Eliminate the data definition in favor of just functions that construct objects with methods inside them.
I have not used any JavaScript-style 'objects' in my code because they are object literals, right. Is there a #4 version of (e.g.) this data definition for an Enigma Reflector?
data Reflector:
| ukw-a with:
method name(self): 'UKW-A' end,
| ukw-b with:
method name(self): 'UKW-B' end,
| ukw-c with:
method name(self): 'UKW-C' end,
sharing:
method alpha(self): get-reflector(self.name()) end,
where:
ukw-a satisfies is-Reflector
ukw-b satisfies is-Reflector
ukw-c satisfies is-Reflector
ukw-a satisfies is-ukw-a
ukw-b satisfies is-ukw-b
ukw-c satisfies is-ukw-c
ukw-a.name() is 'UKW-A'
ukw-a.alpha() is 'EJMZALYXVBWFCRQUONTSPIKHGD'
ukw-b.name() is 'UKW-B'
ukw-b.alpha() is 'YRUHQSLDPXNGOKMIEBFZCWVJAT'
ukw-c.name() is 'UKW-C'
ukw-c.alpha() is 'FVPJIAOYEDRZXWGCTKUQSBNMHL'
end
Furthermore, I'm not sure of the benefit — either from a coding point of view or a pedagogical point of view — of an 'object' like this. (This is the literal object version of my Triple code from the op). Am I doing it wrong? Am I missing something?# literal triple object with its methods
t = {
first: 'first',
second: 'second',
third: 'third',
method rotr(self): {
first: 'third', second: 'first', third: 'second',
} end,
method rotl(self): {
first: 'second', second: 'third', third: 'first',
} end,
method noop(self): {
first: 'first', second: 'second', third: 'third',
} end,
}
check 'triple object':
t.rotr().first is 'third'
t.rotr().second is 'first'
t.rotr().third is 'second'
t.rotl().first is 'second'
t.rotl().second is 'third'
t.rotl().third is 'first'
t.noop().first is 'first'
t.noop().second is 'second'
t.noop().third is 'third'
end
Thanks again.
- dcp
On Tuesday, August 2, 2022 at 11:42:25 AM UTC-4 Ben Lerner wrote:
Hi David,
The answer is,honestly, it depends :) As a programmer, if you have any priorOO programming experience, the methods may feel more natural. Asa teacher, it depends on your curriculum.
Most of the curriculathat Pyret support (e.g. HtDP, DCIC, Bootstrap, etc) tend totake a functional-programming-first approach. In those contexts,obviously the functions will be preferable. To date, Pyret hasmore fully-fleshed-out libraries in the functional style thanthe method style, but we do aim for feature parity in thoselibraries, so that teachers can choose either approach as theyprefer.
In my opinion,methods on data definitions are an intermediate pedagogic point,sitting between “purely functional programming” and “purely OOprogramming”. Methods in Pyret don’t only exist ondata definitions; they can exist on object literals, too:
my-posn = {x: 3,y: 4,method dist-to-O(self): num-sqrt(num-sqr(self.x) + num-sqr(self.y)) end}check:my-posn.dist-to-O() is 5endIf you then have someother functionality in your program that relies on “values thathave a dist-to-O method”, then any object with such a methodwill suffice. This is basically the essence of interface-focusedOOP, rather than class-focused OOP. So the pedagogic progressionwould go
- Define inert data, and externallydefine functions on that data using
casesto distinguish the data variants- Migrate those functions into the
sharingclause of a data definition – this still usescasesto distinguish the variants, but allows calling thefunctionality as a method rather than a function.- Partition the shared method intomultiple methods, one per data variant, and eliminate the
casesexpression. This introduces the concept of dynamic dispatch,separately from any distractions about classes, interfaces,other OO whatevers…- Eliminate the data definition infavor of just functions that construct objects with methodsinside them. These are custom constructors now, and anycustomers of the data now need to talk about what methods theyexpect to see, rather than what constructor they expect thedata to come from. This is interface-oriented programming.
As I said, most ofthe curricula we’ve developed don’t go through all these steps,and focus on #1, while our library idiomatically uses 1, 2 or 3.I do hope we figure out how to support #4, eventually, too!
Ben
On 7/29/22 7:52 PM, David Pettywrote:
I am new to Pyret and unclear when and whyone would use methods versus functions witha data definition. Here is some sample code:
# Exploration of data methodsand functions.
The methods / functions rotr, rotl, noop all havethe same semantics. Which approach is preferable and why?
Thanks.
- dcp--
You received this message because you are subscribed to theGoogle Groups "Pyret Discuss" group.
To unsubscribe from this group and stop receiving emails fromit, send an email to pyret-discus...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/pyret-discuss/b2906414-ca3a-4156-ba67-19e89bd71a10n%40googlegroups.com.
--
You received this message because you are subscribed to the Google Groups "Pyret Discuss" group.
To unsubscribe from this group and stop receiving emails from it, send an email to pyret-discus...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/pyret-discuss/b03f0cec-1207-4280-9c67-afdec74afd55n%40googlegroups.com.