Re: fun v. method

74 views
Skip to first unread message

Ben Lerner

unread,
Aug 2, 2022, 11:42:25 AM8/2/22
to pyret-...@googlegroups.com, David Petty

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

  1. Define inert data, and externally define functions on that data using cases to distinguish the data variants
  2. Migrate those functions into the sharing 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.
  3. Partition the shared method into multiple methods, one per data variant, and eliminate the cases expression. This introduces the concept of dynamic dispatch, separately from any distractions about classes, interfaces, other OO whatevers…
  4. Eliminate the data definition in favor of just functions that construct objects with methods inside them. These are custom constructors now, and any customers of the data now need to talk about what methods they expect to see, rather than what constructor they expect the data to come from. This is interface-oriented programming.

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

On 7/29/22 7:52 PM, David Petty wrote:

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.

Message has been deleted

Shriram Krishnamurthi

unread,
Aug 7, 2022, 10:37:23 AM8/7/22
to pyret-...@googlegroups.com
Hi David,

This is pretty exemplary code! Very neat.

I'm curious what drove your decision to make some things methods. Most of your methods don't seem to use the `self` parameter, which suggests they could just as well have been functions, and it's a stylistic choice. Some seem to quite naturally be useful as trivial methods, like the methods that are toString equivalents (eg, `name`): the datatype-to-string pain had to be endured somewhere, whether as a function or method. I think your encoder are the only true methods in the code.

Note that typing methods is much harder than typing functions. So you're also surrendering the benefits of static typing.

Shriram

Ben Lerner

unread,
Aug 7, 2022, 10:51:53 AM8/7/22
to pyret-...@googlegroups.com, David Petty

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.

On 8/7/22 10:21 AM, David Petty wrote:

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 5end

If 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

  1. Define inert data, and externallydefine functions on that data using casesto distinguish the data variants
  2. Migrate those functions into thesharingclause of a data definition – this still uses casesto distinguish the variants, but allows calling thefunctionality as a method rather than a function.
  3. 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…
  4. 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.
--
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.

David Petty

unread,
Aug 7, 2022, 11:51:01 AM8/7/22
to Pyret Discuss
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 variants are new entries in the namespace and have associated methods. I may have too much of an OO orientation, but it seems natural to have these datatypes w/ individual or shared methods because I can put in type-checked lists and invoke methods without knowing which of the variants I have.

> 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. Is there a #4 version of (e.g.) this data definition for an Enigma Reflector?

David Petty

unread,
Aug 7, 2022, 11:58:02 AM8/7/22
to Pyret Discuss
Thanks for the responses (to a post I deleted because it was incomplete). I just reposted it after completing the thought.

- dcp

Reply all
Reply to author
Forward
0 new messages