Lisp-family: how to escape object-oriented java-like thinking? [closed]
Asked Answered
N

4

11

Backstory: I've made a lot of large and relatively complex projects in Java, have a lot of experience in embedded C programming. I've got acquainted with scheme and CL syntax and wrote some simple programms with racket.

Question: I've planned a rather big project and want to do it in racket. I've heard a lot of "if you "get" lisp, you will become a better programmer", etc. But every time I try to plan or write a program I still "decompose" the task with familiar stateful objects with interfaces.
Are there "design patterns" for lisp? How to "get" lisp-family "mojo"? How to escape object-oriented constraint on your thinking? How to apply functional programming ideas boosted by powerful macro-facitilties? I tried studying source code of big projects on github (Light Table, for instance) and got more confused, rather than enlightened.
EDIT1 (less ambigious questions): is there a good literatue on the topic, that you can recommend or are there good open source projects written in cl/scheme/clojure that are of high quality and can serve as a good example?

Nephron answered 20/12, 2015 at 17:8 Comment(3)
Here's some interesting reading on why design patterns in Lisp(-like) languages are less pervasive than in other languages: norvig.com/design-patternsReefer
One method is to just start writing from the top down, and assuming you have functions that will do what you want. Once you've defined the problem as some sort of calculation more functional idioms tend to fall out.Hamulus
The OOP design patterns can all be mapped onto Lisp, its just that in Lisp they are shorter or less visible. However, yeah just using lambdas, higher-order functions, returning multiple values, and macros can result in code that is much more succint than OOP code. norvig.com/design-patternsTetrasyllable
T
8

A number of "paradigms" have come into fashion over the years: structured programming, object oriented, functional, etc. More will come.

Even after a paradigm falls out of fashion, it can still be good at solving the particular problems that first made it popular.

So for example using OOP for a GUI is still natural. (Most GUI frameworks have a bunch of states modified by messages/events.)


Racket is multi-paradigm. It has a class system. I rarely use it, but it's available when an OO approach makes sense for the problem. Common Lisp has multimethods and CLOS. Clojure has multimethods and Java class interop.

And anyway, basic stateful OOP ~= mutating a variable in a closure:

#lang racket

;; My First Little Object
(define obj
  (let ([val #f])
    (match-lambda*
      [(list)         val]
      [(list 'double) (set! val (* 2 val))]
      [(list v)       (set! val v)])))

obj           ;#<procedure:obj>
(obj)         ;#f
(obj 42)
(obj)         ;42
(obj 'double)
(obj)         ;84

Is this a great object system? No. But it helps you see that the essence of OOP is encapsulating state with functions that modify it. And you can do this in Lisp, easily.


What I'm getting at: I don't think using Lisp is about being "anti-OOP" or "pro-functional". Instead, it's a great way to play with (and use in production) the basic building blocks of programming. You can explore different paradigms. You can experiment with ideas like "code is data and vice versa".

I don't see Lisp as some sort of spiritual experience. At most, it's like Zen, and satori is the realization that all of these paradigms are just different sides of the same coin. They're all wonderful, and they all suck. The paradigm pointing at the solution, is not the solution. Blah blah blah. :)


My practical advice is, it sounds like you want to round out your experience with functional programming. If you must do this the first time on a big project, that's challenging. But in that case, try to break your program into pieces that "maintain state" vs. "calculate things". The latter are where you can try to focus on "being more functional". Look for opportunities to write pure functions. Chain them together. Learn how to use higher-order functions. And finally, connect them to the rest of your application -- which can continue to be stateful and OOP and imperative. That's OK, for now, and maybe forever.

Tafoya answered 20/12, 2015 at 18:30 Comment(0)
B
6

A way to compare programming in OO vs Lisp (and "functional" programming in general) is to look at what each "paradigm" enables for the programmer.

One viewpoint in this line of reasoning, which looks at representations of data, is that the OO style makes it easier to extend data representations, but makes it more difficult to add operations on data. In contrast, the functional style makes it easier to add operations but harder to add new data representations.

Concretely, if there is a Printer interface, with OO, it's very easy to add a new HPPrinter class that implements the interface, but if you want to add a new method to an existing interface, you must edit every existing class that implements the interface, which is more difficult and may be impossible if the class definitions are hidden in a library.

In contrast, with the functional style, functions (instead of classes) are the unit of code, so one can easily add a new operation (just write a function). However, each function is responsible for dispatching according to the kind of input, so adding a new data representation requires editing all existing functions that operate on that kind of data.

Determining which style is more appropriate for your domain depends on whether you are more likely to add representations or operations.

This is a high-level generalization of course, and each style has developed solutions to cope with the tradeoffs mentioned (eg mixins for OO), but I think it still holds to a large degree.

Here is a well-known academic paper that captured the idea 25 years ago.

Here are some notes from a recent course (I taught) describing the same philosophy.

(Note that the course follows the How to Design Programs curriculum, which initially emphasizes the functional approach, but later transitions to the OO style.)

edit: Of course this only answers part of your question and does not address the (more or less orthogonal) topic of macros. For that I refer to Greg Hendershott's excellent tutorial.

Bewilderment answered 20/12, 2015 at 19:0 Comment(6)
This does not really apply to something like the Common Lisp Object System, where its easy to add both classes or methods due to the mix of open classes, multiple inheritance, trivial mixins, no interfaces, method combinations, multi-methods, ... etc.Zecchino
@RainerJoswig Of course you are right. Many "solutions" exist for the "expression problem" but I think it is still a useful portrayal of the two styles for those who are unfamiliar.Bewilderment
The 'expression problem' is a problem with statically typed languages with closed classes. Most Lisp object systems don't fall into that category.Zecchino
@RainerJoswig It is still instructive to be aware of the "problem", so that one may understand the tradeoffs associated with the "solutions". Open classes of course have their own tradeoffs, for example they limit the amount of static reasoning and optimizations one may perform.Bewilderment
But that has not much to do with Lisp vs. OO, because it is not OO vs. Lisp. Lisp has absorbed/enhanced OO in various forms since 40 years. Lisp is not even especially 'functional'... Lisp also most of the time has limited static reasoning in multiple ways. Static/closed and statically typed OO is just one way of OO. It's not in Smalltalk, Javascript, Python, Ruby, and also not in most Lisp-based OO.Zecchino
@RainerJoswig Yes, my answer compares OO vs FP at a basic fundamental level, which is how I interpreted the question, and I said as much. My intent was not to compare any specific languages. I only associate Java with "OO" and Lisp with "functional" because the OP did but of course very few languages actually fall cleanly into one "paradigm". I agree that in some sense Java is not even "real" OO in the historical sense but perhaps that discussion deserves a separate answer on its own.Bewilderment
S
2

The "Gang of 4" design patterns apply to the Lisp family just as much as they do to other languages. I use CL, so this is more of a CL perspective/commentary.

Here's the difference: Think in terms of methods that operate on families of types. That's what defgeneric and defmethod are all about. You should use defstruct and defclass as containers for your data, keeping in mind that all you really get are accessors to the data. defmethod is basically your usual class method (more or less) from the perspective of an operator on a group of classes or types (multiple inheritance.)

You'll find that you'll use defun and define a lot. That's normal. When you do see commonality in parameter lists and associated types, then you'll optimize using defgeneric/defmethod. (Look for CL quadtree code on github, for an example.)

Macros: Useful when you need to glue code around a set of forms. Like when you need to ensure that resources are reclaimed (closing files) or the C++ "protocol" style using protected virtual methods to ensure specific pre- and post-processing.

And, finally, don't hesitate to return a lambda to encapsulate internal machinery. That's probably the best way to implement an iterator ("let over lambda" style.)

Hope this gets you started.

Selfdrive answered 20/12, 2015 at 17:38 Comment(2)
So who is the fifth gang member?Reefer
Ringo Starr, of course.Selfdrive
I
2

A personal view:

If you parameterise an object design in the names of the classes and their methods - as you might do with C++ templates - then you end up with something that looks quite like a functional design. In other words, functional programming does not make useless distinctions between similar structures because their parts go by different names.

My exposure has been to Clojure, which tries to steal the good bit from object programming

  • working to interfaces

while discarding the dodgy and useless bits

  • concrete inheritance
  • traditional data hiding.

Opinions vary about how successful this programme has been.

Since Clojure is expressed in Java (or some equivalent), not only can objects do what functions can do, there is a regular mapping from one to the other.

So where can any functional advantage lie? I'd say expressiveness. There are lots of repetitive things you do in programs that are not worth capturing in Java - who used lambdas before Java provided compact syntax for them? Yet the mechanism was always there.

And Lisps have macros, which have the effect of making all structures first class. And there's a synergy between these aspects that you will enjoy.

Insight answered 20/12, 2015 at 18:26 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.