(toiminnot)

hwechtla-tl: Funktionaalisen ja oliopohjaisen suunnittelun eroista

Kierre.png

Mikä on WikiWiki?
nettipäiväkirja
koko wiki (etsi)
viime muutokset


(kategoria: ohjelmointi)

Minun kommenttini ovat tässä kursiivilla:

In the functional world there is the idea of a module that seems very similar to an object to me. They are not the same, but I am unable to tell much of a difference. Can anyone explain the difference?

I'll try.

The bottom line is, modules are the way of choice to encapsulation / code organization, higher order functions are the way to dynamic binding. Of course, eg Ocaml adds to this the object system which provides nice type system facilities which are found in neither of the above.

Your points:

Yes, this is often used when it is not important for the data structure to dictate its own behavior at runtime, which is surprisingly often.

I can't answer this properly because it is so much a matter of taste. I've never seen a use of inheritance (except inheriting from interfaces, which is equivalent to filling up a record of functions) that does not make the software harder to maintain and read in the long run. And whenever possible, I do favor giving behavior (ie dynamic binding) as a parameter rather than put it into data structures, unless the point of those data structures is to contain behavior. This makes the code easier to reason about.

Thanks for the elaboration. It seems, as you mentioned, a different take on encapsulation. I don't really understand though why it's ok to have keys map to multiple attributes (records, tuples, lists), yet not to functions as well. That's basically all this/self do. It's just a shorthand for stuff that needs to be passed around in a functional program. Guess I don't see a big deal difference in the end.

Hm. Because functions are values, there really is no distinction.

Anyway. I think sometimes functions are quite okay to put into data structures (after all, a first-class function is a very simple data structure containing a function), sometimes it's okay to mix functions and other data in data structures and sometimes even make state changes. It's just that more often than not, I'll regret doing it unless I really had to do so.

WRT "just a shorthand..." this, I disagree on. Even leaving aside designs enabled by LazyEvaluation, which are different from most OO designs indeed, functional programs tend to be arranged very differently from OO ones. One usually builds very generic tools that get specialized as high in the code hierarchy as possible (by giving them parameters that specialize them). In OO, the specialization is on the lowest level of the code hierarchy, because all data is coupled with the routines that are supposed to deal with it. Both models have their uses, but I find them quite different.


If you can pass in a function in at the highest level, then that specifies what must happen at the lowest level, because whatever level uses the function must know about the function and use it. How is that different?

It differs in where the behavior is specified. I'll try and give a simple example (unfortunately, this is somewhat geared towards functional). Let's say we're formatting a list into HTML. This is too simple to actually benefit from factoring like this, but anyway:

 bracket_with_actions left right fn arg = left >> fn arg >> right

 sequence actions = foldl (>>) (return ()) actions
 iterate = sequence . map

 output_list_html list =
         bracket_with_actions (print "<ul>") (print "</ul>")
            (iterate
              (bracket_with_actions (print "<li>") (print "</li>") print)))
            list

It might be hard to see why make it like this (and indeed somebody might say this is not good practice), but it is easy to see how to (again) make the output_list_html function more generic by passing the brackets and/or the item printing function as a parameter. So the specialization endlessly boils up:

 output_list beg end left right pr list =
         bracket_with_actions (pr beg) (pr end)
           (iterate (bracket_with_actions (pr left) (pr right) pr)))
         list

 output_list_ul = output_list "<ul>" "</ul>" "<li>" "</li>" print

 output_list_table = output_list "<table><tr>" "</tr></table>" "<td>" "</td>"
                        print

The point is to keep the low level routines generic but intelligent, so you have an ever-growing repertoire of services you can use for new tasks. In this way, functional languages are superb for OnceAndOnlyOnce - you can factor anything to a separate function, loop patterns, function-defining functions, and so on...

Let's look at the (equally overkill) OO solution:

 class HtmlListNode:

        def __init__(s, value):
                s.value = value

        def output_as_list(s):
                print "<li>%s</li>" % s.value

        def output_as_table(s):
                print "<td>%s</td>" % s.value

 class HtmlList:

        def __init__(s, nodes):
                s.nodes = nodes

        def output_as_list(s):
                print "<ul>"
                for el in s.nodes: el.output_as_list()
                print "</ul>"

        def output_as_table(s):
                print "<table><tr>"
                for el in s.nodes: el.output_as_table()
                print "</tr></table>"

Now the behavior is specified at the same level as the data. One can also have different kinds of behaviors in the same list. The code is clearly not as mobile and refactorable as the code above, and I'm second-guessing what kind of interface I should provide for the list.

I could emulate the functional solution with formatter objects, which is arguably what should be done here. But what kind of structure will list then have, is it not to protect itself from outside access? And building helpers like bracket_with_actions is relatively cumbersome in non-functional languages. And the OO code isn't even parameterized with respect to printing, so I have to add new methods if I want to output to another media. Or should this be handled by an outputter object? Why does the OO solution suddenly have more things to pass down?

It would seem that the functional code is restricted by its use of ">>" as a nonchangeable sequencing operator. But this is a generic monadic operator, which can take many meanings depending on the monad we work in...

Normally you would write to a stream object which can be tied to many different types of output. That is far more useful than printing to no place in particular. After you are done can you count the number of characters? Can you apply another transform that must be applied to a completed document? Can you send the output to a file, browser, and socket?

Of course. Just pass something other than print there.

By the way, how would you handle this in an OOP setting? Would you give the stream object as an argument, or would you have something less functional-style? In a functional language, on the other hand, even if I had not anticipated the need to print to various places in various ways, I could easily do this by changing the monadic environment the list handlers work in...


I just don't see it. I want to see it. I just don't. The functional approach in your example doesn't seem the best way to solve the program on many fronts.

What do you mean by "solving the program"?

It isn't the best way to do everything, but then it isn't the only way of programming FP supports - others include OOP, monadic style(s), FunctionsAsData, InfiniteDataStructures, etc... I just wanted to present you one typically functional style of arranging a program, as opposed to the particularly object-oriented style of arranging it. At no stage was the point to prove the superiority of FP, just its feasibility and the differences between the two ways of seeing the program.

If you really want to see the light, I might not be the best person to talk to - after all, I'm not even trying to convert you. Besides, one doesn't learn much by listening to people they're already suspicious about. The learning comes by using multiple ways to structure problems. Grip Ocaml, for example, read the tutorial, get going. If you don't like the syntax, try Scheme instead. Read example code. Compare different ways of doing the same thing. That way, one learns. BTW, Haskellists are particularly good at (ab)using functional features.

I really do appreciate your effort and I realize you aren't trying to convert me. But I am trying to be converted. I guess I take FPs feasibility (at least non-pure) for granted. Now I'm trying to see what works better imho. Sure, if I needed laziness then FP would be better, but that's not the type of problem I usually work on. I like pattern matching, built in tuples, etc. Love erlang's process concept and message passing. I use map stuff in OO all the time, but some syntactic sugar would be nice. Yet the OO method of encapsulation seems clearer to me. Modules just go half way, imho.

Laziness is one of these things: you don't miss what you don't use. For me, laziness is a way to get away from the "what it does" domain into the "what it produces" domain. This is a tremendous performance boost for me. Moreover, lazy evaluation enables ways of programming that would not otherwise be possible. It works as a communication mechanism between functions with local state (thus providing yet another answer to your "state threading" question), enables separating of end conditions from loop code, and so on.

As I do a lot of embedded programming we need more control over resource usage.

WRT modules: when you first break referential transparency (which is what you usually do with every object - object usually have changeable state), you drop from the domain of reliable, constant services into the domain of practical services that correspond to something you don't have control over. They no longer work as the building blocks of your arbitrary program logic, but as the building blocks of the model of the problem domain. They are bound to the problem domain, and are suspect to domain changes, incorrect domain analysis, and so on.

What does this mean in practice? You think: "hmm. I have a clear concept (say a user in an ircd-like product). Should I give it its own state to handle destructively, or should I keep the state? Should it talk directly to its neighbors, or should it just return what it has to say?" If you do the latter, you might end up cursing the passing of state back and forth (though you might want to model this with coroutines / messagefilter / somesuch). But if you do the former, you're going to have to poke the user object for everything: changing connections, rollback actions, persistency, and any ad-hoc queries you might have, to name a few. And if you guess wrong which neighbors it needs, you have to rearrange your program heavily. And every service's code is split in two parts which make their own integrity constraints. The user object is endlessly doomed to have state and to be unusable as a generic service and limited in how it can be used for building new services.

I guess I don't see this. If you could be specific. It's like the html example. You pass in some functions and the same thing is attainable from an abstract base class. A change does not impact the code any more or less. In fact, the class solution is more maintainable because there are a lot fewer things being passed around. In OO you poke an object, in FP you get the data from somewhere and pass it around. If the user account has changed then all the other clients need to know about the change. Copies aren't good for that sort of thing.

You say the class solution is more maintainable, but my experience says contrariwise. There are many ways to reduce the number of things passed around. And in functional programming, you often don't need to change the behavior of a specific service (other than to correct misbehavior), because they are just services and you can build a new service whenever the functionality provided by the old one is not what you need. The services don't "own" anything you would need access to by that specific service exactly.

WRT abstract base classes: I thought you were well aware that abstract base classes references _are_ practically function pointers (to functions that possibly have associated data), which in turn are practically functions without closures. Because you can emulate closures with the implementation-object-associated data, first-class functions and references to abstract base classes are semantically equivalent. But the building of closures creates so much clutter in OO code (just watch my example at OnMonads) that it becomes infeasible. So the difference is not so much in the underlying mechanisms but in the way you arrange the program.

That was part of my point. The differences aren't great enough to get all excited about.

Now you are getting to a wholly unrelated point. Yes, OO is not technically weaker than FP. But if I need no more lines (of code) to make objects from functional concepts than I would need to make objects in some language called "object-oriented", while I have to write tens of lines of extraneous code whenever I want to emulate a closure, that, I think, matters. For me the experience of writing in, say, Java after writing in Ocaml is like, "why the $@!¤# do I have to do all this?" Just go and try. Don't waste your time arguing with me.


kommentoi (viimeksi muutettu 23.01.2013 16:26)