Question about data structures and encapsulation

249 Aufrufe
Direkt zur ersten ungelesenen Nachricht

Colin Yates

ungelesen,
15.06.2011, 14:41:5215.06.11
an clo...@googlegroups.com
Newbie so go gentle please :).  

I am an experienced OO Java developer (decade +) considering jumping fence to a functional language, and clojure is pretty high up on the list for a number of reasons.

I am so used to defining everything as objects which are sealed units of state and behaviour that I am struggling to see how to solve the same problem with clojure.  I desperately wish somebody would write a "domain driven design with clojure" :).

In brief, in OO state is exposed via a well defined API.  That state may be simple properties (values) or it may be calculations (functions).  And critically, the decision as to whether it is a value or a function is an implementation concern.  The Java Bean spec defines accessors for properties of a class, behind which lies the logic of how to retrieve that state.  So, the very common Person class will expose get/setName(), get/setAge() etc. and as a consumer I have no idea how the results are calcualted.

In Clojure, if I understand correctly, the preferred way would be to use a map (or defstruct) with keys such as :name and :age.  These are then retrieved as (person :name) and (person: age) etc.  

My question is if I suddenly decided that one of those values is best implemented as a calculation, how can I seamlessly implement that.  By seamless I mean implement it without updating any consumers of a person?  For example, if I changed the age property to be  the result of a function, I could either replace the value of age with a function that calculates age or write a function(person)->age.

Both of those are disruptive to the consumers of person.

I understand that clojure is about explicitly distinguishing between state and functions, but I see this as a high price to pay.  Have I missed something?  The OO in me is saying "well, never introspect a map directly, rather provide get-X(person) functions" but that is very very noisy.

That's enough for now - this is, I expect, the first of many cries for help :)

Thanks in advance to all who reply!

Ken Wesson

ungelesen,
15.06.2011, 15:00:5215.06.11
an clo...@googlegroups.com
On Wed, Jun 15, 2011 at 2:41 PM, Colin Yates <colin...@gmail.com> wrote:
> In Clojure, if I understand correctly, the preferred way would be to use a
> map (or defstruct) with keys such as :name and :age.  These are then
> retrieved as (person :name) and (person: age) etc.
> My question is if I suddenly decided that one of those values is best
> implemented as a calculation, how can I seamlessly implement that.  By
> seamless I mean implement it without updating any consumers of a person?

>  For example, if I changed the age property to be  the result of a function,
> I could either replace the value of age with a function that calculates age
> or write a function(person)->age.
> Both of those are disruptive to the consumers of person.
> I understand that clojure is about explicitly distinguishing between state
> and functions, but I see this as a high price to pay.  Have I missed
> something?  The OO in me is saying "well, never introspect a map directly,
> rather provide get-X(person) functions" but that is very very noisy.

But that's more or less what you'd have to do. If age might be
calculated in some more complex manner, now or in the future, you want
something like

(defn age [person]
(:age person))

or whatever.


--
Protege: What is this seething mass of parentheses?!
Master: Your father's Lisp REPL. This is the language of a true
hacker. Not as clumsy or random as C++; a language for a more
civilized age.

Meikel Brandmeyer

ungelesen,
15.06.2011, 15:11:0115.06.11
an clo...@googlegroups.com
Hi,

and just today this was posted to reddit: http://skillsmatter.com/podcast/scala/talk-by-patrick-fredriksson

Sincerely
Meikel

Laurent PETIT

ungelesen,
15.06.2011, 16:03:5615.06.11
an clo...@googlegroups.com
Hi,

I must admit my thoughts are still not fixed concerning this, guided
by several considerations:

* consider the ring spec: it specifies what keys are expected to be
present in the request map, the response map. Good enough.
* Wait ! what if some keys could be calculated from others (derived)
=> they may not be essential, put them in a function.
* Wait ! what if the computation of the value of the derived key heavyweight ?
* answer 1 : let the consumer "cache" it.
* answer 2 : "cache" the value in the map, but make it clear that
it costs something to compute it, e.g. by placing it explicitly (as in
"in the spec of your map") in a `delay` construct
* choice between 1 and 2 will obviously be in the library's designer hand
* I tend to be very liberal with the use of maps inside the library
I'm writing : after all, no other code than my library will depend on
it, so I assume the choice of breaking inner parts of my lib by
exposing map keys everywhere in it. It's a kind of cost/benefit
tradeoff: no cost upfront, many benefits, and if later I have to
change more things in my lib than if I had encapsulated things, then I
both grumble and then think that the "price" of the change has been
paid several times by not having paid the cost of having encapsulated
all parts of my lib "concepts".
* I tend to be more selective with the parts of the lib which are
exposed to consumers.
* All in all, it may not be such a big deal, because of the
following characteristic of clojure: it emphazises representing in
maps only the "essential" pieces of your domain model. Those which
will be subject to change for "good reasons" (change in spec), not
"wrong reasons" (hopefully). And, also, when it's possible, I try to
only expose as maps to consumers as little as possible. The more
"objects" they retrieve from libraries are "opaque objects" (only
intended to be passed back to library functions), the better. This is
related to the previous point: the only "objects" which remain
"transparent" are then the maps which represent the "essential" part
of the concept (only data which are not computable from other data).

HTH,

--
Laurent

2011/6/15 Colin Yates <colin...@gmail.com>:

> --
> You received this message because you are subscribed to the Google
> Groups "Clojure" group.
> To post to this group, send email to clo...@googlegroups.com
> Note that posts from new members are moderated - please be patient with your
> first post.
> To unsubscribe from this group, send email to
> clojure+u...@googlegroups.com
> For more options, visit this group at
> http://groups.google.com/group/clojure?hl=en

Sean Corfield

ungelesen,
15.06.2011, 18:12:3815.06.11
an clo...@googlegroups.com
Hi Colin! Welcome to Clojure!

On Wed, Jun 15, 2011 at 11:41 AM, Colin Yates <colin...@gmail.com> wrote:
> the very common Person class will expose get/setName(), get/setAge() etc.
> and as a consumer I have no idea how the results are calcualted.

The FP approach certainly takes some getting used to after a lot of
Java! I like the fact that a lot of (post-Java) languages go out of
their way to avoid all the boilerplate get/set methods in various ways
because this is very high ceremony.

I was lucky enough to be exposed to functional programming in the 80's
and then move into OOP in the 90's (with C++ in '92 and Java in '97).
Whilst I am still "unlearning" some OOP habits, I'm more comfortable
with the non-OO approach and dusting off my older programming
approaches. I'm finding, as I introduce more people to Clojure, that
folks without a lot of OOP experience tend to pick up FP much quicker
and aren't as concerned about get/set "encapsulation".

My experience with OOP has been that essential properties of objects
rarely change into computed functions (and derived properties are
computed functions in the first place). That said, of course there are
situations where an essential property needs a function: when you have
side-effects on the get/set operation, such as recording changes, but
I'd argue that is likely to be known upfront. Also given the
preference for immutable data, you're much less likely to use/need
setters.

My suggestion would be to code with raw maps instead of
functions-wrapping-maps and see how things go. By using raw maps
you'll find that a lot of power can be brought to bear with standard
Clojure functions.

As for the age property on Person, that seems to be a common example
given to justify the use of getters but age is a derived property,
based on date of birth, and every real world system I've worked with
that represents people, and needs age, uses a function for it upfront.
So I think that's a bit of a strawman :)

Hope that helps?
--
Sean A Corfield -- (904) 302-SEAN
An Architect's View -- http://corfield.org/
World Singles, LLC. -- http://worldsingles.com/
Railo Technologies, Inc. -- http://www.getrailo.com/

"Perfection is the enemy of the good."
-- Gustave Flaubert, French realist novelist (1821-1880)

Colin Yates

ungelesen,
15.06.2011, 20:59:3915.06.11
an clo...@googlegroups.com
Thanks for all the help, all of you.  The Clojure community has a reputation for being helpful :)

The example of age as a property which might change from a value to a function was indeed a strawman, but it was just an example.  So the consensus seems to be that yes, that requirement is hard to solve, but as Sean states, it isn't a particularly common occurrence.  KISS with maps seems to be the way to go.

Thanks again!

Alessio Stalla

ungelesen,
16.06.2011, 05:02:1816.06.11
an Clojure
FWIW, in Common Lisp the two main record-like types of objects
(structs and classes) are defined using macros that have the option of
generating accessor functions for you. Also typically when using raw
lists as data structures it is considered good practice to define
functions that abstract the access to their elements.
If defining functions manually for your maps is tedious, consider
coding a macro that does it for you. Note that, unlike accessor
methods in OO classes, accessor functions for maps/lists cannot be
polymorphic in the type of the data structures they access, so they
typically end up encoding the name of the data structure in their own
name, e.g person-age.

Cheers,
Alessio

Vincent

ungelesen,
16.06.2011, 05:42:0716.06.11
an clo...@googlegroups.com
 nice podcast 

thanks

Mikkel

ungelesen,
16.06.2011, 09:20:0416.06.11
an Clojure
This could be one way to solve the problem in the example and keep a
uniform API:
(def joe {:age ((fn [] (- 2011 1979)))})
(:age joe)
#> 32

Any comments on that?

Laurent PETIT

ungelesen,
16.06.2011, 09:39:5516.06.11
an clo...@googlegroups.com
2011/6/16 Mikkel <mik...@gmail.com>:

> This could be one way to solve the problem in the example and keep a
> uniform API:
> (def joe {:age ((fn [] (- 2011 1979)))})
> (:age joe)
> #> 32
>
> Any comments on that?

You're applying the function immediately. It's not different than doing
(let [age ((fn [] (- 2011 1979)))]
{:age age})

Meikel Brandmeyer

ungelesen,
16.06.2011, 09:48:2216.06.11
an clo...@googlegroups.com
Hi,

one could use lazymap: https://bitbucket.org/kotarak/lazymap. Unfortunately, it is broken at the moment. Have to bring up-to-date with 1.2 and later.

(defn person
  [name year-of-birth]
  (lazy-hash-map :name name :age (- 2011 year-of-birth)))

The actual difference would only be calculated when the :age key is accessed. The result would then be cached. (It uses delays under the hood).

Sincerely
Meikel

Caleb

ungelesen,
16.06.2011, 14:01:2716.06.11
an Clojure
Colin,
I just read this definition from SICP, which made me think about your
question again:

"In general, the underlying idea of data abstraction is to identify
for each type of data object a basic set of operations in terms of
which all manipulations of data objects of that type will be
expressed, and then to use only those operations in manipulating the
data."

So that seems pretty compatible with your definition, except that as
you said, in OO your operations typically have shared state in
addition to shared data types.

But I am not sure that I agree that having functions that operate on
the data, rather than sharing state are more noisy. For example,
these seem fairly equivalent to me:

p/age(caleb)
p/favorite-color(caleb)

or

caleb.getAge()
caleb.getFavoriteColor()

This assumes that you have defined 'age' and 'favorite-color' in a
namespace that you have aliased as 'p' in the current context. That's
an important point to me, because In addition to providing operations
on shared state, objects and classes are also used to create
namespaces for operations. In Clojure you can get namespaces a la
carte (without having to use classes). I am fairly beginnerish at
Clojure myself, but I have found its namespaces to provide some of the
important conceptual glue that I relied on classes for in other
languages. Otherwise you would have a lot of noise.

SICP reference: http://mitpress.mit.edu/sicp/full-text/book/book-Z-H-14.html#%_sec_2.1.2

Ryan Twitchell

ungelesen,
17.06.2011, 00:58:5917.06.11
an Clojure
Regarding JavaBeans and the like, more often than not I've found that
when getters and setters do not reference fields directly, they are
contributing to a composition: as an adapter, a facade, or whatever-
you-please. It is not quite as often that getters simply return a
calculation based on data in their own object. With that in mind,
note that such compositions usually address the problems of
structuring a program in some new way, often at run time. Functional
programming has lots of its own solutions for such problems.

Just for reflection:
What would you do with an existing class which had a getName() method,
when you suddenly realized you wanted getFirstName() and getLastName()
instead? Or the reverse?
What would you do if the problem you described applied to a database
table?
What would you do if you wanted to process several unrelated classes
in a generic manner, reading data from all of them? How about an
arbitrary number of existing, unrelated classes discovered at runtime?

Clojure does encourage programming to interfaces (and protocols), and
how much state you expose is up to you. Consider balancing this risk
by including (for defrecords) a library of functions which achieve
your client code's most common needs regarding a record type or a set
of related types (you could probably describe this as an application
of the Pareto principle). Try not to pepper such a library with
getter functions without good reason though. The problem you pose is
a real risk when data can be directly accessed, there is no avoiding
that; every programming language has its trade-offs, just as every
design pattern does.

Here are a few of what I've found to be the most useful pieces of
information for appreciating Clojure's functional style, coming from
OO. The first two, at least, you've probably found already. ;)

Directly related to your question, a clojure.org page that discusses
the disadvantages of information hiding: http://clojure.org/datatypes

"Object Orientation is overrated": http://clojure.org/rationale

Stuart Halloway's Protocols and OO presentation
(ClojureProtocolsJAOO.pdf), available from
http://github.com/stuarthalloway/clojure-presentations/downloads

Out of the Tar Pit, a 60 page essay on "functional/relational
programming". Not a light read, but convincing. -
http://web.mac.com/ben_moseley/frp/frp.html

Ryan

Ken Wesson

ungelesen,
17.06.2011, 02:56:2417.06.11
an clo...@googlegroups.com
On Fri, Jun 17, 2011 at 12:58 AM, Ryan Twitchell <metat...@gmail.com> wrote:
> Just for reflection:
> What would you do with an existing class which had a getName() method,
> when you suddenly realized you wanted getFirstName() and getLastName()
> instead?  Or the reverse?

If you can't refactor the class, that's a relatively simple one to
solve with helper methods/functions:

(defn word-seq [s]
(map (partial apply str) (take-nth 2 (partition-by
#(Character/isWhitespace %) s))))

(defn is-not-capitalized [w]
(Character/isLowerCase (first w)))

(defn get-name [first last]
(str first " " last))

(defn get-first-name [full]
(first (word-seq full)))

(defn get-last-name [full]
(let [chunks (reverse (drop 1 (word-seq full)))]
(apply str
(interpose " "
(reverse
(cons
(first chunks)
(take-while is-not-capitalized (rest chunks))))))))

=> (get-name "Bob" "Marley")
"Bob Marley"
=> (get-first-name "Bob Marley")
"Bob"
=> (get-last-name "Bob Marley")
"Marley"
=> (get-last-name "Sarah Michelle Gellar")
"Gellar"
=> (get-last-name "Frederique van der Wal")
"van der Wal"
=> (get-last-name "Olivia d'Abo")
"d'Abo"

The only slightly tricky one is get-last-name, since some last names
are more than one word and these need to be distinguished from middle
names. Fortunately, these last names only seem to capitalize the last
chunk. The last three examples use three famous actresses' names to
demonstrate that it drops middle names, keeps multi-part surnames, and
keeps surnames that start with a noncapitalized letter but are only
one part.

Of course there may be some more obscure corner case that isn't
covered, and if the input is incorrectly capitalized or spaces are
missing, all bets are off.

Names with only one chunk are presumed to be first names, so
get-first-name returns the whole input and get-last-name an empty
string in those cases.

Note how the above is clean sequence-manipulation code without a
single ugly regexp in sight.

--
Some people, when confronted with a problem, think “I know, I'll use
regular expressions.”
Now they have two problems.
- Jamie Zawinski

Colin Yates

ungelesen,
17.06.2011, 05:28:3817.06.11
an clo...@googlegroups.com
Interesting points.  Thanks for the pragmatic advice.

Your statement "With that in mind,

note that such compositions usually address the problems of
structuring a program in some new way, often at run time.  Functional
programming has lots of its own solutions for such problems" sums up my issue - I feel blind to those problems.  In particular, I find myself asking "how would I implement this OO solution in Clojure" which is obviously the wrong question!

I am trying to learn the lessons before committing a significant chunk of code, but as ever, I think I need to get my hands dirty.

Again, thanks for taking the time to reply!

Col

Christian Schuhegger

ungelesen,
17.06.2011, 22:56:4417.06.11
an Clojure
Thanks a lot for the link to the paper about FRP! My personal thinking
is going 90% in the same direction that the paper describes. I am
happy to see that somebody else did the hard work of writing it
down :)

Is anybody aware of an implementation of such an approach for Clojure?
Allen antworten
Antwort an Autor
Weiterleiten
0 neue Nachrichten