> ... Are there any unique aspects/features/functions of the Lisp paradigm
> that would be good/useful additions to Smalltalk?
About a week ago there was a thread titled "Loneliness of Nil".
On hindsight it seems much of the controversy was caused by some
participants thinking about "nil as in Lisp" while others
were - simultaneously - thinking about "nil as in Smalltalk".
In Lisp nil is (more or less) equal to an "empty list".
In Smalltalk nil is best thought of as "undefined value",
since "OrderedCollection new" is already a good representation
of an empty list. And because a variable that was never given
a value, returns nil when you ask for that value.
This I think points to an interesting difference between
(more or less pure) functional languages like Lisp, and
Object Oriented languages like Smalltalk, in general.
Now it's over a decade since I worked with Lisp, so I'll
appreciate any counter arguments from the Lisp community,
to what follows...
In a functional language everything is computed by a function.
A program really is just a big function which depending on the
arguments returns the value of some sub-function. A function can
return a list of values, and that list can be empty, i.e. nil.
The semantics of an empty list can be interpreted as 'the answer
to your question is: There are none'. For instance if you called
a function that returns a person's middle initials, you might
receive an empty list / nil as the answer, meaning the person has
no middle initials in their name.
The alternative interpretation of "Your question doesn't make sense"
seems less natural, because if we adopt it, how could we represent
the other case ("there is none!")?
I may be wrong but it now seems to me that in pure Lisp there is
no such concept as 'undefined value'. There is only the nil, which
should be interpreted as "no values", rather than "unknown value".
In Smalltalk in contrast we have 'nil' to represent "unknown
information" but also 'Collection new' to represent "no values".
These are clearly two different things, with different semantics.
If I'm right (which sometimes happens), it would seem that OO-
languages have a distinct advantage over functional languages
here.
In Smalltalk it is easy to 'reflect' on a program since
we have 'nil' to signify that something is "missing from"
the OO model represented by the Smalltalk program. This
is used frequently in 'lazy initializers' which first use
reflection to realize that a variable has no value, and
if so create a default value for it.
Also, I can override #notUnderstood: so that any message
without a corresponding defined method returns 'nil' to signify
that the program has "no information" about how to interpret
the request. This means something like "This attribute is
undefined" or "Request not understood" - as opposed to:
"I understand the question and I'm able to tell you
the answer is: 'none'".
An OO program seems to be more capable of "knowing what it doesn't
know", than a functional program, which is simply what it is, a big
function with several components.
I may be wrong, but I guess my question boils down to:
1. "What is the equivalent of Smalltalk's #notUnderstood: in
functional languages?"
2. "How do you distinguish between 'missing information', and
known information that the 'answer is an empty list' ?"
-Panu Viljamaa
"A computer running Smalltalk seems more conscious"
1. "What is the equivalent of Smalltalk's #notUnderstood: in
functional languages?"
It depends on the language. The closest thing in Scheme is an error;
the closest thing in many dialects is an exception.
In both cases, the idea is to shelter the "ordinary" flow of control
from this issue. In the case of exceptions, the idea is provide
programmers the option of recovering flow of control.
2. "How do you distinguish between 'missing
information', and known information that the 'answer
is an empty list' ?"
Mostly as above.
You have a weird way of looking at nil, in that you insist on reducing
it to an english noun-clause.
English is not particularly great at encoding data structures.
In programming, we define structures of a mathematical nature -- the
art being to define a structure that is a good model of the
application at hand. Simultaneously, we seek data structures that are
convenient to manipulate in a program.
If computer programming has contributed at all to the intellectual
development of humans, it is in the area of such modeling. We
programmers have found models that could not have been found if we
confined ourselves to "natural language". We are inventing new
natural language to talk about the new models. There is no
substitute for understanding computational math other than to
understand computational math.
Convenience of manipulation is an important consideration. Not too
long ago, I had a mini-epiphany while working with some Emacs lisp
code.
The epiphany was this: the "liberal" use of nil (as false, as end of
list, as a nil-returning argument to CAR and CDR) is damn convenient.
I made the decision to take advantage of `(car nil) => nil' and so
forth in this program. The result was that the code accomplished
more, with less work. I wound up with algorithms that were perfectly
transparent, when read presuming that various values were not nil --
but that also worked with nil. You could read the algorithm first,
presuming not-nil, and get the gist. Then you could ask "what happens
if such-and-such value is nil" and, much more often than not, the
Right Answer simply fell out of the same algorithm. Gosh the RnRS
authors are full of hot air in this area :-)
Nothing is False (alas, precious little is permitted)
-t
--------
Nerf is Dead. Long Live Nerf. A finer and prouder kitchen cat was
never born. Long Live Maxwell, His Brother, who, in spite of the
limitations of cat cognition, has taken sorrowful notice of His
Absense.
Nerf died, in part, of poverty. In Nerf's name, may the Revolution
live on, and flourish.
When the distinction is important the functional language can return more
than one value. Here's a hash table example:
(setf hash-table (make-hash-table))
(gethash 'unknown-key hash-table)
Returns two values: nil and nil.
The first nil denotes no value associated with the key.
The second nil denotes that no key exists.
(setf (gethash 'key hash-table) nil)
(gethash 'key hash-table)
Returns two values: nil and t. That is the symbol key exists (t) and is
associated with the value nil.
> In Smalltalk it is easy to 'reflect' on a program since we have 'nil' to
> signify that something is "missing from" the OO model represented by the
> Smalltalk program. This is used frequently in 'lazy initializers' which
> first use reflection to realize that a variable has no value, and if so
> create a default value for it.
>
> Also, I can override #notUnderstood: so that any message without a
> corresponding defined method returns 'nil' to signify that the program has
> "no information" about how to interpret the request. This means something
> like "This attribute is undefined" or "Request not understood" - as
> opposed to:
>
> "I understand the question and I'm able to tell you
> the answer is: 'none'".
>
> An OO program seems to be more capable of "knowing what it doesn't know",
> than a functional program, which is simply what it is, a big function with
> several components.
>
> I may be wrong, but I guess my question boils down to:
>
> 1. "What is the equivalent of Smalltalk's #notUnderstood: in
> functional languages?"
>
> 2. "How do you distinguish between 'missing information', and
> known information that the 'answer is an empty list' ?"
By returning an additional value to clarify the status of the empty list.
Usually there is no need to make such a distinction. But even if there is,
why can't a non-nil symbol denote a missing value--let's call it
missing-information. Then you might argue that there could be information
that's similar to missing-information so we need a way to distinguish
between missing-information and information that is missing but is not
missing-information. Again you could return a second value to eliminate
the ambiguity.
While I don't know much about the OO-paradigm you could also do this in
Lisp:
(defstruct info missing answer)
(make-info :missing t :answer nil)
This returns the structure #S(info :missing t :answer nil). It's easy to
find out the values of info-answer and info-missing.
Don't forget that Lisp is multi-paradigm. So if the answer is "OO is
better" then the Lispers can take off their functional programming hat and
agree with you. The same with imperative programming or iteration.
Regards,
Adam
[...]
PV> In Lisp nil is (more or less) equal to an "empty list".
In Lisp, NIL is primarily the `null' reference. It then becomes
the empty list, because the null reference is the natural value for
the last tail of a list. True, technically the value of NIL is an
object (the symbol which is the value of (INTERN "CL:NIL")), but
not `conceptually.' (And that object is (to be treated as)
immutable.)
PV> In Smalltalk nil is best thought of as "undefined value",
PV> since "OrderedCollection new" is already a good representation
PV> of an empty list.
I don't know Smalltalk that well, if at all, but isn't
"OrderedCollection new" an object?
This is an important point. An ordered collection in Lisp would be
a sequence. An empty ordered collection would then be a sequence
whose length is 0. There are two possibilities: the empty list,
and a vector of length 0. Now, if you want to destructively add an
element to an empty sequence, you just cannot do that with the
empty list in Lisp, but you can, at least in a number of cases, do
that with a vector of length 0 (for example, if it has a fill
pointer, or if it is adjustable). In that sense, Lisp's empty list
is not an object, while an empty vector is.
(I have probably screwed up the terminology somewhat, but I hope at
least the point is clear. A `sequence of length 0' is, of course,
a value S such that (= (LENGTH S) 0).)
PV> And because a variable that was never given
PV> a value, returns nil when you ask for that value.
If we are talking about a special variable in Lisp, then it would
be unbound in such a case, which is different from it having a
value of NIL, and which is a situation where it is an error to
access its value.
If we are talking about a lexical variable, then it will be
implicitly initialized to NIL if the programmer did not provide an
explicit initial value, but I don't know if this is analogous to
the situation in Smalltalk.
There is another difference between Lisp and other object-oriented
languages (yes, Lisp is object-oriented, and not just because of
CLOS). Again, I don't really know how it is in Smalltalk, but in
Java, for example, null is a value that is of any (object) type,
while in Lisp NIL is a value of only two types, NULL (whose only
value is NIL) and LIST (which is a special case anyway, being the
union of NULL and CONS).
So, for example, if in Lisp a variable has been declared as (say) a
string, it is an error if that variable is set to NIL (this error
may or may not be actually signalled depending on a number of
circumstances, but that is a different matter). In Java, on the
other hand, it is perfectly OK for a String variable to have a
value of null, and it in fact happens quite often. (This, combined
with the fact that you cannot invoke a method off null, leads to a
rather unwelcome proliferation of things like
``(s != null)? s.substring(b, e) : null''.)
[...]
PV> 2. "How do you distinguish between 'missing information', and
known information that the 'answer is an empty list' ?"
There isn't a `one size fits all' solution.
Sometimes just returning NIL is good enough.
Sometimes the programmer may provide a value to be returned for
`missing information' (as in GET's last argument, or READ's
eof-value argument), which would default to NIL.
Sometimes an additional value would be returned, serving as a
`missing information' flag (see e.g. SUBTYPEP).
Sometimes an error would be signalled for the case of missing
information.
(I would also like to note that I am not very concerned which is
the True Way of Functional Programming, or the True Way of
Object-Oriented Programming, for choosing the `missing information'
value.)
---Vassil.
--
For an M-person job assigned to an N-person team, only rarely M=N.
There has been a certain amount of fussing in the Scheme community
over this issue in the last year (although arguably I have done much
of the fussing). The real question has more to do with how much
of the type lattice do you want to reify in your language.
The notion of "undefined value" is closely analogous to the notion of
"indeterminate" or "divergent" value as represented in the literature
by the symbol _|_ (pronounced bottom). In Scheme, this "value" is
expressible by the expression:
(if #f #f)
which, of course, has no value for the else branch of the
conditional. Fortunately (or unfortunately depending on your POV) the
Scheme standard has chosen not to reify this value with a concrete
syntax. This is annoying to me as a programmer because it does make
lazy initializers more complex to implement (primarliy when using
macros). The various language implementors like (well approve of) its
absence because it means that they can perform more aggressive
optimization.
Actually the trade-offs in Scheme are a little more involved than
this (because there is an implicit recursion through a universal union
type - just as in Smalltalk), but that's the general outline.
> In a functional language everything is computed by a function.
This is also true in an OO language. First-class lexical closures
gives you all the machinery you need to completely generate Smalltalk,
modulo syntax. I generally don't bother to go that far when I need OO
structures in my Scheme code. I generally stop around the complexity
of Self.
> A program really is just a big function which depending on the
> arguments returns the value of some sub-function. A function can
> return a list of values, and that list can be empty, i.e. nil.
There is an important distinction between a function which returns a
list of values and a function which returns multiple values. It is IMO
better not to think of nil as the empty list, but more as the base
case for the recursive datatype of binary trees (Lisps actually use a
degenerate tree structure for representing lists, but that's a whole
different argument).
> The alternative interpretation of "Your question doesn't make sense"
> seems less natural, because if we adopt it, how could we represent
> the other case ("there is none!")?
>
> I may be wrong but it now seems to me that in pure Lisp there is
> no such concept as 'undefined value'. There is only the nil, which
> should be interpreted as "no values", rather than "unknown value".
The concept is there (see above). It has not been syntactically
reified, and it is semantically unreliable. This is a problem, IMO,
and I actually appreciate the clarity with which you state the
issue. And nil is simply nil, the interpretation depends pretty much
entirely on context.
> If I'm right (which sometimes happens), it would seem that OO-
> languages have a distinct advantage over functional languages
> here.
You're right, but you generalize too broadly. Smalltalk has a (slight)
advantage over Scheme in this. I say slight, because it is fairly easy
to work around, and some of the work-arounds actually promote a better
type-discipline in your programs, anyway.
> Also, I can override #notUnderstood: so that any message
Yes. I've even done this kind of horrible thing (and worse, I added
methods to class UndefinedObject) in Smalltalk. That is a feature
whose use needs to be *strongly* discouraged (except of course when I
do it <grin/>).
> An OO program seems to be more capable of "knowing what it doesn't
> know", than a functional program, which is simply what it is, a big
> function with several components.
If what you mean to say is that the failure modes are softer in
Smalltalk, then I'm inclined to agree with you; however, that is not
necessarily a good thing. Soft failure modes frequently hide bugs.
If you're trying to anthropomorphize your programming environment,
then I think that you're missing a far more basic point of
engineering.
> 1. "What is the equivalent of Smalltalk's #notUnderstood: in
> functional languages?"
Various other run-time errors. Some functional languages eliminate the
need for such things by construction. Some functional languages give
you access to very high-powered machinery (first-class continuations)
for constructing many different kinds of error and condition handling,
most give you exception handling.
It's actually a poorly-formed question. A better question is "how do
you handle unimplemented functions in a category?", or the more
limited "how do you handle method dispatch errors?". You need to reach
deeper with your understanding of the Smalltalk semantics to find the
common ground in the functional world.
> 2. "How do you distinguish between 'missing information', and
> known information that the 'answer is an empty list' ?"
By convention (in Scheme #f and '() are both commonly used to
represent _|_). By explicitly checking predicates, rather than
performing nil checks after the fact. By programming in explicit CPS
or otherwise providing (exception) handlers for the cases 'missing
information' cases. By constructing your datatypes such that the
missing information cases have an explicit representation. Lotsa ways,
really.
david rush
--
In a profession plagued by, "when all you have is a hammer, everything
looks like a nail," we get really excited when someone is able to come
along and prove that everything really *is* a nail if lambda is the
hammer. -- Bruce R Lewis (on comp.lang.scheme)
> (if #f #f)
>
> which, of course, has no value for the else branch of the
> conditional. Fortunately (or unfortunately depending on your POV) the
> Scheme standard has chosen not to reify this value with a concrete
> syntax. This is annoying to me as a programmer because it does make
> lazy initializers more complex to implement (primarliy when using
> macros). The various language implementors like (well approve of) its
> absence because it means that they can perform more aggressive
> optimization.
I don't see what difference it makes. What's the difference between giving it
a *syntax* in the language and
(define-syntax unspec
(syntax-rules ()
((unspec) (if #f #f))))
?
Perhaps you mean giving it some semantics other than those described in the
last sentence of 1.3.2 of R5RS.
On 25 Oct 2002 18:46:39 -0400, Vassil Nikolov <vnik...@poboxes.com>
made a stupid mistake:
VN> (the symbol which is the value of (INTERN "CL:NIL"))
^^^^^^^^^^^^^^^^^
As it was kindly pointed out to me, the above is incorrect and
should be (INTERN "NIL" (FIND-PACKAGE "CL")). My bad.
> ... If you're trying to anthropomorphize your programming environment,
> then I think that you're missing a far more basic point of
> engineering.
Not the programming environment, but the objects in my Object-Oriented
program. I understand there's a place for 'engineering black box imple-
mentation'. But there's also a need to create high-level models of the
world, sometimes called 'Business Models'. The advantage of high-level
languages is that such models need not be expressed in UML-diagrams
(only),
but the OO program can be the model itself.
When modeling the world, people and organizations I think it
is important to be able to make distinction between "Answer is
'nothing'" and "Answer is unknown". In ST it seems quite natural
to interpret nil as "answer unknown". In Lisp we can adopt a
convention like "the atom 'unknown' represents an unknown answer",
but that is somewhat ad hoc. I think this level of ad hocness is
the only real difference. Yet I think it is caused not by specific
language implementations, but by more basic differences in the
functional and OO paradigms. Mostly by the treatment of state.
Taking this a bit further, I can always ask a person any question,
and they can always answer "I don't understand your question".
Again Smalltalk has a built-in facility for this: #notUnderstood: .
In Lisp if I call a (named) function which does not exist, this is
naturally interpreted as an error.
Thanks for everyone for the comments.
-Panu Viljamaa
The issue is not between Functional and OO paradigms. It is between a
broken part of Lisp and a correct part of Smalltalk. In fact, the
functional paradigm far more accurately describes the meaning of
'answer unknown' (as you put it). I don't get the impression that you
really grok FP (pardon me if I'm wrong here); it took some real
programming in SML (during my copious free time while I was a
super-productive Smalltalker ;) for me to begin to get it. Suffice it
to say that I am convinced that FP subsumes OOP when it comes to
theoretical *and* practical modelling of computation.
I will admit that it's nice to be able to fall back on the more
limited model (OOP) when I have to deal with more limited minds ;)
> Taking this a bit further, I can always ask a person any question,
> and they can always answer "I don't understand your question".
> Again Smalltalk has a built-in facility for this: #notUnderstood: .
> In Lisp if I call a (named) function which does not exist, this is
> naturally interpreted as an error.
And the default implementation of #notUnderstood: invokes the
debugger; pretty effectively signalling an error. Oddly enough, a Lisp
REPL has much the same level of interactivity, although it obviously
lacks the fancy GUI (which is *not* to be sneered at - Smalltalk is
great fun).
david rush
--
The Revolution has begun...Comrade Shivers's helicopter will land on
College Green at 3.14pm. Deployment of the Underground begins soon
thereafter. Lambda bandanas will be available...Please remain calm
and nobody will get skewered on parentheses. -- sk (on c.l.s)
Note that in comp.lang.scheme, this is considered flamebait, even
though its truth is obvious in comp.lang.lisp. It's best to leave the
Schemers to their logical purity, and keep them away from CAR NIL
knowledge. ;)
> I made the decision to take advantage of `(car nil) => nil' and so
> forth in this program. The result was that the code accomplished
> more, with less work. I wound up with algorithms that were perfectly
> transparent, when read presuming that various values were not nil --
> but that also worked with nil.
The Schemer credo is that because (car nil) blows up, it helps in the
discovery of bugs in list manipulation code. I don't understand how,
but each to his own.
> .... I don't get the impression that you really grok FP ...
Well, that's why I'm asking. From the outset it seems to me that
to represent 'unknown information' we also need to have an
easy way of modeling the state-change of some information
becoming known. "Information Unknown" in other words means
"at the current state and time, this information is not available".
Let's say I ask an object "what is the address of so-and-so?".
The anwwer is 'nil' , for representing the fact that the address
is unknown. Then the person registers her address over a web-site.
If I ask the object the same question, now the 'state of information'
has changed to 'known'.
So what would be the easiest, simplest, most human-grokkable
way to represent this change of state in a purely functional program ?
Thanks
-Panu Viljamaa
well, imho its not so much that but the fact that '() is not
a pair - and car / cdr are defined as operating on pairs and
hence there is no reason to expect car/cdr to operate on '()
Otherwise you would have to define '() as the immutable pair
which both "car" and "cdr" slots pointed to itself - imho not
nice at all.
--
Sander
+++ Out of cheese error +++
There are easy answers to this - which you've already heard - and then there
are the philosophical answers, which may be more long-winded (or perhaps
that's just me) but may also be illuminating.
The short of it is that your "information unknown" answer corresponds to a
function with an undefined value for certain inputs. For a mini-essay
explaining why this must be the case, and what the implications are, read
on.
> Let's say I ask an object "what is the address of so-and-so?".
> The answer is 'nil' , for representing the fact that the address
> is unknown.
Functional languages are based on mathematical concepts. The mathematical
concept to which your example most closely corresponds, is that of a partial
function. A partial function is not defined for all elements of its domain,
i.e. it is undefined for at least some elements.
It's important to understand what "undefined" means. It's not as simple as
just being a value which can be given a denotation in a language so that,
for example, evaluating "x / 0" returns a special "undefined" value. This
confuses levels of operation - the function returns a value in cases for
which it is defined, but in cases for which it is not defined, it does not
return a value - its return value is undefined. (If "undefined" is a value
in the language, it creates an ambiguity in my last sentence, which hints at
the philosophical problem here.) Representing undefined operations by a
special value within a language raises all sorts of mathematical and
philosophical issues. For good reasons along these lines, such cases are
most commonly modeled as an error in functional languages. Errors imply an
"out of band" response, rather than "just another value", and that's
theoretically important.
Exploring the "x / 0" example used above, division by zero is considered an
error in most languages, not just in FP. Smalltalks tend to have a whole
class dedicated to this momentous error. The reason it's an error is
because the mathematical operation, division, is not defined for division by
zero. This is a case where both "nil" and "not understood" would be
inappropriate. A program which redefined division to return some special
value in the case of division by zero, as opposed to signalling an error,
would be wrong in many respects. It can and has been done, but it's never a
good idea.
The functional approach says that your case "what is the address of
so-and-so?" should be treated exactly the same way as division by zero.
You're asking a question for which no answer exists, which implies a partial
function. Asking such a question is an error, by definition. That doesn't
mean you can't handle such an error in your program, but the instant you
decide to implement a special non-existence value and return that from a
function, you're deviating philosophically from a pure functional approach.
Returning "nil" as the answer to such a question, from the functional
perspective, would mean that the answer to the question *is* known, and that
it is "nil".
> Then the person registers her address over a web-site.
>
> If I ask the object the same question, now the 'state of information'
> has changed to 'known'.
You would then be dealing with a different function. This function would be
defined for a value for which its predecessor was not defined. It's fairly
natural to think of this as the "same" function with slightly different
behavior from its previous incarnation, but that's a kind of mental
shortcut, which should not be confused with mathematical "reality".
Mathematically, it is not the same function, by definition: the way in which
it maps input values to output values is different, so it can't be the same
function.
This change is very similar to the way the OO object equivalent would have
changed. An object whose state has changed in this way might be considered
the "same" object from the point of view of its "identity", but if you had a
copy of the original object and the modified object, and compared them using
a deep equality operation, they would be different. This reflects the fact
that the previous version of the object and the new version are different
objects, in some sense, even though they share "identity" in the OO sense.
> So what would be the easiest, simplest, most human-grokkable
> way to represent this change of state in a purely functional program ?
"Most human-grokkable" is a subjective criterion. The concept of errors
signalled by partial functions is very human-grokkable, and very useful in
practice. It is also very similar to the situation with objects in a number
of important respects. However, since object systems don't tend to model
things from as mathematical a perspective, they often provide solutions
which may not make much mathematical sense (or at least, aren't
mathematically optimal). Returning "not understood" instead of signalling
an error what is meant is that the answer is undefined, would be one such
case.
You've raised the issue of state quite a bit. The reasons for the handling
of undefined values in FP languages have very little to do with state.
However, comparing FP and OO has everything to do with state. Comparing
object approaches to pure functional (mutation-free) approaches requires a
clear understanding of how state is modeled in both approaches. One very
significant thing that object languages like Smalltalk do, which pure
functional programs do not intrinsically do, is to assign an identity to
objects whose state changes over time. In Smalltalk, you might model a
single person as a single (composite) object, even though many of the
person's basic attributes and relationships change over time.
The OO notion of identity is a modeling construct that is imposed on systems
of processes which occur over time (this comes from the view that objects
are merely slow processes). There are large elements of arbitrariness in
this modeling activity, and that is one of the problems that OO designers
have to grapple with - exactly what object model to impose on the processes
they are trying to model. In a sense, it is an attempt to create manageable
staticness where there isn't really any, or emphasize the relative
staticness in processes that exists at certain levels - e.g. over
small/large timescales, or small/large distances.
One of the tricks of OO modeling is to figure out which things are static
enough to be modeled as though they have identity, and how to divvy up the
borderlines so that this works out. (In politics, this is called
gerrymandering :) Some things are relatively easy to do this with - people,
products, companies. Other things are not as clear-cut - clouds and flames
are nice examples, and in fact you find many such examples in scientific
models, fewer in business models.
To summarize, objects are in fact an enormous hack for modeling the effects
of state change over time in a particular way. If you don't realize that,
you're not as good an object programmer as you could be. (Once you do
realize it, you might become a functional programmer ;)
It's misleading to imagine that objects somehow universally manage this
state modeling better - that perspective comes from looking at the issue
from "within the hack" - from within a single way of dealing with the
problem. To see this, you have to step fully outside that model - you can't
simply attempt to directly map particular ways of modeling state change onto
other systems, and then decide that they are less sensible when you find
that your preferred approach makes less sense in other models.
None of this means that you have to eschew the use of objects in order to
use FP. Languages like Smalltalk, in addition to providing fundamental
language features, provide large class frameworks which are in many ways
closely tied into the core of the language. The question you've asked
extends across the fuzzy border between the class framework and the
language, in Smalltalk's case. That's not the case with FP.
Functional languages, or mathematics itself, provides a rigorous,
unambiguous answer to the question of what a function can do with inputs for
which they are not defined. However, nothing stops you from building
systems on top of this very well-defined foundation, and those systems can
behave in any way you want them to. Many functional languages have object
systems, often based around the concept of subtype polymorphism, which OO
borrowed from mathematics. In many of those object systems (or in systems
you define yourself), you can (ab)use nil-like values as much as you want
to. The FP languages just have clearer boundaries between their object
systems and the underlying languages. As any OO modeler can tell you,
decoupling like this is good.
Anton
There is no reason to expect + to operate on mixtures of integers,
rationals *and* floating points, either, or for PRINT to operate on
everything.
> Otherwise you would have to define '() as the immutable pair
> which both "car" and "cdr" slots pointed to itself - imho not
> nice at all.
Or, what is worse, you might have to adjust your thinking and accept
the idea that the properties of a pair object are not defined by the
inputs and outputs of the CAR and CDR functions. That something can be
passed to CAR does not make it a pair. Similarly, that something can
be added with an integer does not make it an integer, or that
something can be printed does not make it a string of text.
There is *a* reason to expect it to operate on numbers (as defined
by number?) - if this was not so, having a full numerical tree
would become a lot less meaningful. I would agree if + was defined
in terms of integers - except it isn't.
>
>> Otherwise you would have to define '() as the immutable pair
>> which both "car" and "cdr" slots pointed to itself - imho not
>> nice at all.
>
> Or, what is worse, you might have to adjust your thinking and accept
> the idea that the properties of a pair object are not defined by the
> inputs and outputs of the CAR and CDR functions. That something can be
> passed to CAR does not make it a pair. Similarly, that something can
> be added with an integer does not make it an integer, or that
> something can be printed does not make it a string of text.
R^5RS:
procedure: (car pair)
Returns the contents of the car field of pair.
+ is a function defined to take parameters of type number, car (and cdr)
are defined to take a parameter (and not three parameters or ten or ...)
of type pair (and not string or ...)
Strictly going by the text of R5RS its more correct to take the car of
a string than a car of '() and the first is not explicitly notes as
being an error.
> ... Functional languages, or mathematics itself, provides a rigorous,
> unambiguous answer to the question of what a function can do with inputs for
> which they are not defined.
I assume from what you've said, the answer is: "A function
can't do anything with inputs for which it is not defined".
But this is not how the world we observe around us works.
For instance, a human being who doesn't understand a given
question will typically ask for a clarification (not 'crash').
I think it's great if we can model the world with equations,
but I think it's impossible - or too cumbersome - to create
rigorous mathematical models in many cases. Therefore the
emergence of such tools as OO/simulation languages.
> ... However, nothing stops you from building systems on top of this very
> well-defined foundation, and those systems can behave in any way you want them
> to.
Nothing prevents us from building systems on top of Turing Machines,
which are a well-defined foundation indeed. Yet we rarely do so in
practice. So basing the choice of a programming language on the
mathematical rigour that comes with it seems questionable.
-Panu Viljamaa
> The Schemer credo is that because (car nil) blows up, it helps in the
> discovery of bugs in list manipulation code. I don't understand how,
> but each to his own.
I think this is because of the standard Scheme null? predicate. This
is the prefered way to test for an empty list (I don't really know
what advantages it has over (eq? x '()) - maybe interpreter speed
tricks?) - because of this, the rationale is you shouldn't ever need
to take the car/cdr of the null list, and if you are, you likely
missed the end-case conditional and are going to get stuck in an
infinite loop thanks to mr. tail recursion.
There are also a couple of bastard half-predicate, half-list
list-extraction procedures (memq and friends) that return a boolean
false if they don't find what they're looking for - although this
actually strengthens NIL's case because it would be more general and
consistent.
Yes. Hardly a very controversial statement, and completely analogous to the
object case you're talking about. The difference is between returning
"nil", and raising an exception. In fact, returning nil can be viewed as a
way of raising an exception, although as I've mentioned, this confuses
things happening at different levels.
> But this is not how the world we observe around us works.
> For instance, a human being who doesn't understand a given
> question will typically ask for a clarification (not 'crash').
An exception is not a crash. It's not even an "error", per se, although it
can eventually result in one. An exception can and should be handled
gracefully, and the result could easily be "ask for clarification".
Functional approaches are no worse at modeling "how the world we observe
around us works" - the approach I'm describing actually does so better than
the approach you've described in the above sentence, since you've conflated
the human being with the process of answering the question, and reached a
wrong conclusion as a result.
> Nothing prevents us from building systems on top of Turing Machines,
> which are a well-defined foundation indeed.
FYI, re "well-defined foundation indeed", Turing machines are no better
defined than the lambda calculus, on which functional languages are based.
Unlike Turing machines, lambda calculus provides a tractable basis for the
foundation of computing and computing languages, which has proven itself
over and over in practice.
> Yet we rarely do so in practice.
When it comes to functional languages and approaches, you're incorrect.
Some examples:
* Smalltalk's block closure capability is directly based on functional
concepts - the lexical scoping & closure behavior is straight out of lambda
calculus, which was mathematically specified nearly 40 years before
Smalltalk was invented. Early computer language inventors had to somewhat
reinvent the concepts, since like you, they didn't initially see the
relevance of the mathematics, but the end result is that every language with
lexical scoping follows the same mathematical rules. If you've ever found
code blocks useful, well, welcome to functional programming, albeit in a
rather limited sense. If you really want to understand them, their
implications, and why they work the way they do, study functional
programming.
* Advanced compilers of languages like C++ use a technique call static
single assignment (SSA), to convert variable assignments to a mutation-free,
closure-based form. This form derives its mathematical properties from
lambda calculus, and allows the compiler writers to perform well understood,
meaning-preserving mathematical transformations of the code, to achieve
optimizations that would otherwise be difficult or impossible to perform.
The functional approach here provides a very powerful model of a very
complex problem domain.
* Relational databases provide a very successful example of mathematical
concepts applied in the real world, and map closely to traditional
functional language concepts.
* The functional concept of continuations has been used to replace
system-level threads in operating systems (Mach research project) and
languages (Scheme, SML/NJ). Continuations can provide equivalent
performance to system threads, with lower resource usage. They allow things
that are traditionally considered impractical, such as transparently
converting the stateless behavior of web servers into low overhead stateful
behavior, or transferring an executing process from one machine to another.
* Many of the functional languages implement object systems of some kind,
and many of them do so in the functional language itself. If you have
concerns about the performance of this, take a look at Doug Bagley's
Shootout Scorecard:
http://www.bagley.org/~doug/shootout/craps.shtml, in which functional and
multi-paradigm languages like OCaml, SML, Scheme and Lisp dominate the
charts, alongside C and C++. OCaml and Lisp have standardized object
systems as part of the language, Scheme and Lisp have more optional object
systems than you can shake a stick at, and SML and OCaml have traditional
functional typesystems.
> So basing the choice of a programming language on the
> mathematical rigour that comes with it seems questionable.
That's not the right way to look at it. It's not about choice of
programming language. It's about learning how to use a variety of powerful
tools for analyzing and modeling the problems you have to deal with.
Smalltalk itself can be modeled mathematically, from a functional
perspective, and one gains understanding of it by doing so - including an
understanding of what it's *not* good at. Only once you have an appropriate
conceptual toolset, can you rationally choose a language based on its
features, although by then, you'll also have found that the concepts apply
across languages.
If you've ever had to program in a procedural language, you might have found
that applying OO ideas in that language helps you model things in a more
coherent way than might have been the case otherwise. Functional concepts
similarly apply across languages. Once you understand them, you might also
better understand the tradeoffs Smalltalk makes, and you'll be able to write
better Smalltalk code as a result.
The history of programming languages has largely been one of retroactively
applying increasingly stronger formal understanding to often ad-hoc
languages. That process isn't over, by a long shot. If you want to know
where programming languages are going over the next couple of decades, i.e.
a big chunk of your career, study functional languages now. That's not to
suggest that all languages will be functional in a decade's time; but
rather, that languages will continue to benefit from better understanding of
their formal underpinnings, which exist whether you know about them or not.
Arguing against having a good formal understanding of these things is like
arguing that you don't need to know any physics to be a good engineer. You
sound to me as though you're looking for reasons to avoid learning anything
more about functional programming. If you're really trying to learn, the
way to do it is not a feature-by-feature comparison between languages - you
have to learn to write real code in at least one language, and read some of
the books and papers out there to gain a decent grasp of the concepts. A
good step for a Smalltalker exploring functional concepts would be to learn
Scheme, since it shares some familiar features with Smalltalk, but
encourages functional thought. Think of it as a gateway drug for functional
languages. Some of the classic books which use Scheme to teach important
computer science and functional concepts are SICP, HTDP, and EOPL (all three
acronyms come up first in a Google search).
Here's a good article that argues for a better understanding of these
topics: "Role of Programming Languages Study in Programmer Education"
(Friedman): http://www.cs.indiana.edu/~dfried/mex.ps or
http://www.cs.indiana.edu/~dfried/mex.pdf That should at least move the
focus beyond whether returning nil from a function is the ultimate decision
factor for choice of language.
Anton
> ...Once you understand them, you might also
> better understand the tradeoffs Smalltalk makes, and you'll be able to write
> better Smalltalk code as a result.
That is precisely my intention. I'm specifically interested in
seeing how some of the problems I've proposed here would be better
solved in Haskell for instance, and why they would be better done
in Scheme than in Smalltalk.
Is there perhaps a short example somewhere that shows how specifically
a program in a pure FP language is better, and why, than an equivalent
program written in Smalltalk?
> ... Arguing against having a good formal understanding of these things is like
>
> arguing that you don't need to know any physics to be a good engineer.
I definitely don't argue against a good formal understanding - of anything.
(where did you get that?)
But I've found that by asking questions about practical examples
is often the fastest way for me to understand something new.
I understand the value of these functional techniques in practice,
from working with Smalltalk in particular:
- No side-effects means easier debugging
- Block-closures make it easy to create iterators and other
dynamically created functions.
What else should go on this list?
What I'm now trying to figure out is "Would I be better off
with a 'pure' fp language, and why?". Why not use a language
that offers both functional- and OO capabilities?
> You sound to me as though you're looking for reasons to avoid learning
> anything
> more about functional programming.
On the contrary. I've been asking a lot of questions - in order
to learn more about how the pure FP languages differ from Smalltalk
for instance. What are the pros and cons. I appreciate all answers
I've gotten, including your current one which contains a lot of good stuff.
Thanks
-Panu Viljamaa
The problem is, it's not as simple as that. Solutions to small problems
tend to be quite similar across languages, and this doesn't tell you much
about bigger strengths and weaknesses. And "strengths and weaknesses" are
the point - it's not as simple as "all pure FP languages are much better
than Smalltalk" (although that might be true from certain formal
perspectives).
> But I've found that by asking questions about practical examples
> is often the fastest way for me to understand something new.
I think there's a problem with that approach when you're dealing with a very
different "paradigm" (if you'll excuse the buzzword which has long ago been
chewed up & spit out by the object world). The question is exactly how new
(to you) the thing you're trying to understand is. If it's similar to what
you're used to, then little micro comparisons may be useful. I think that's
the wrong way to approach learning FP, though, if you don't have much
background in that area. Although different people certainly have different
learning styles, I think you'd be much better off tackling it as though
you're learning something truly new - pick up a book or tutorial and start
working through it, and try to forget what you think you already know. One
of the biggest barriers to learning can be out-of-place preconceptions like
"that's not the way the world works".
> I definitely don't argue against a good formal understanding - of
anything.
> (where did you get that?)
Sorry if I misunderstood. My sense was that you were using this small
example of returning nil - a fairly trivial issue in any overall sense - as
a way to conclude something like "therefore FP isn't as friendly". That
seemed to me like a very flimsy straw man, which aside from being of
questionable validity, completely misses many more important issues.
> What I'm now trying to figure out is "Would I be better off
> with a 'pure' fp language, and why?". Why not use a language
> that offers both functional- and OO capabilities?
Choosing a hybrid, for real-life use, is perfectly reasonable. To me the
importance of FP is that it's the area from which current formal
understanding of computer languages derives - in many respects, the concepts
are much more important than the actual languages. Once you understand the
concepts, you are better informed to decide things like when mutation of
state is appropriate and when it's bad, when the language you're using is
preventing you from using a more powerful solution, when objects are a good
solution vs. when functions and closures make sense, and when to choose
static over dynamic typing.
While learning, though, it's a good idea to try a pure approach. It's hard
to fully understand new benefits if you allow yourself to continue doing
things the old way, like C programmers using C++ as "a better C". This is
where one or more of the books like SICP/HTDP/EOPL can help, because they
take you through the concepts with some discipline - all of them avoid
mutation initially, and by the time it is introduced, if you've really been
following your reaction should be "ugh! why would anyone want to do *that*
??"
Whether you code in Lisp, Scheme, Haskell, ML, or Ocaml, you can make use of
FP concepts effectively. That's not to say those languages are equivalent -
there are some startling differences between them, especially along the
static/dynamic typing faultline, but also in areas like macros, and the
ability of a language to read & understand its own code (an area in which
the Lisps excel).
> - No side-effects means easier debugging
> - Block-closures make it easy to create iterators and other
> dynamically created functions.
>
> What else should go on this list?
I'll leave that for others right now (late night), but one real biggy in the
functional world is the first-class function. Block closures in Smalltalk
come close, but don't quite capture the usage in functional languages. With
first-class functions, "iterators" as you know them are essentially
unnecessary. The functional approach emphasizes functions like map, fold,
reduce, filter, etc. and composition of functions (e.g. given a function
"double" and a function "square" in ML and similar languages, "double o
square" creates a function which calculates 2x^2). A source that might give
a bit of a feel for this kind of thing is the ML tutorial, "A Gentle
Introduction to ML", at
http://www.dcs.napier.ac.uk/course-notes/sml/manual.html
Anton
I don't know what your pre-Smalltalk background was; but I remember
spending a good six months programming, having *already* been
convinced about the advantages of OOA/D/P before I really grasped why
Smalltalk was so much better than C++. For me FP was the same. I
needed to write a substantial program (a resource scheduling program
in SML) before I really grokked why I had everything to gain and very
little to lose from working in the FP world (it worked 100% correct
the *first* time I ran it).
What I'm trying to say is that there's only so much we can tell. Some
of it you have to absorb by building non-trivial programs on your own.
And I would have to disagree that Scheme (as much as I love it) is a
good place to start if you're coming from Smalltalk. You'd end up
wasting time on infrastructure and it doesn't teach the lessons you
need to learn. I really think SML (*not* OCaml, some full SML+module
system) is the way to go. SML's functors lead to a very nice
alternative formulation of OO semantics.
david rush
--
Any clod can have the facts - having opinions is an art
-- Infinite Void (on comp.lang.scheme)
Modern "functional" languages have many other features that are completely
unrelated to the "purity". It is a shame that discussion has focused a bit
too much on this particular feature. For languages like ML one gets the
following.
1. Expressive module system
2. Algebraic datatypes and pattern matching
3. Parametric polymorphism (Generics)
4. Polymoprhic type inference
5. Exception handling
6. Efficient compilers
7. Garbage collection
The OCaml and Haskell crew can enumerate their sets of features that are an
addition to the list above.
I think, in fact that us "functional" programmers are not doing a very good
job of explain what it is all about. Actually, I think it's safe to say that
the "functional" part of the functional programming languages is over
emphasized.
I think what distinguish FPLs from other languages is a design philosophy
more than just purity. In fact LISP and ML are both impure languages, but
the share a similar design philosophy.
I think the design philosophy of FPLs can be summed up as "programming
should be like doing mathematics."
The languages are designed to support formal reasoning. They include very
powerful concepts taken from the mathematical and logical
communities. Purity being just one of them. Higher-order functions and types
and purity are all concepts that came out of the mathematical community and
have be adopted in FPLs because of that. In fact most modern functional
programming languages have very strong connections with logical systems via
I think it depends on the approach being taken. If you're using an
SICP-like book, then the book tends to dictate the language, and Scheme
seems to have an edge in the literature of that kind. If you're just
picking up a language tutorial or reference and coding up a system in a new
language, then there are more alternatives. However, the latter approach is
likely to fail to deliver the kind of attitude adjustment that's really
necessary, or at least do so much more slowly.
> For me FP was the same. I
> needed to write a substantial program (a resource scheduling program
> in SML) before I really grokked why I had everything to gain and very
> little to lose from working in the FP world (it worked 100% correct
> the *first* time I ran it).
>
> What I'm trying to say is that there's only so much we can tell. Some
> of it you have to absorb by building non-trivial programs on your own.
"I hear and I forget. I see and I believe. I do and I understand."
-- Confucius
The + function can be defined in whatever terms you want it to be.
There is indeed a good reason why it is generic to different kinds of
numbers and their combinations. It would be terribly inconvenient to
have it be otherwise.
The B language which preceded C had separate operators for floating
point. They had a # prefix, as in #+ #- #* #/ . That idiocy was
dropped.
> >> Otherwise you would have to define '() as the immutable pair
> >> which both "car" and "cdr" slots pointed to itself - imho not
> >> nice at all.
> >
> > Or, what is worse, you might have to adjust your thinking and accept
> > the idea that the properties of a pair object are not defined by the
> > inputs and outputs of the CAR and CDR functions. That something can be
> > passed to CAR does not make it a pair. Similarly, that something can
> > be added with an integer does not make it an integer, or that
> > something can be printed does not make it a string of text.
>
> R^5RS:
> procedure: (car pair)
>
> Returns the contents of the car field of pair.
>
> + is a function defined to take parameters of type number, car (and cdr)
> are defined to take a parameter (and not three parameters or ten or ...)
> of type pair (and not string or ...)
In Scheme yes; but you see other languages have a CAR function too.
(car nil), or in Scheme terms (car '()), is exactly like (sqrt -1).
You can restrict the domain to real numbers, and throw an error. Or
you can extend the domain to the complex plane and return the
imaginary value i.
The Lisp version of CAR extends the domain from conses to lists, which
include nil in the domain value, and introduces the rule that you can
access the CAR and CDR (also known as FIRST and REST) of an empty
list, producing NIL. There is nothing wrong with this extension of the
list algebra.
People objected to imaginary numbers too when those were discovered;
that's why they are called imaginary. The naysayers eventually had to
cave in, because imaginary numbers turned out to be incredibly
convenient, leading to straightforward computations and
representations where contortions existed previously, for the sake of
avoiding the computation of the square root of a negative number.
In Lisp, we have an imaginary CAR field of the NIL value. It does the
same thing as imaginary numbers: it takes some contortions out of
expressions operating on lists, which exist only for the sake of
avoiding the invocation of CAR or FIRST on an empty list.
Imagine that!
> Strictly going by the text of R5RS its more correct to take the car of
> a string than a car of '() and the first is not explicitly notes as
> being an error.
If you go strictly by the text of R5RS, then you can't think
critically about the design decisions imposed by that text; you are
within the box.
Ultimately, the machine that the code runs on will have different
operators for integers and floating point, so depending on what
level you want to program, having the operators be distinct is
nothing odd.
>
>> >> Otherwise you would have to define '() as the immutable pair
>> >> which both "car" and "cdr" slots pointed to itself - imho not
>> >> nice at all.
>> >
>> > Or, what is worse, you might have to adjust your thinking and accept
>> > the idea that the properties of a pair object are not defined by the
>> > inputs and outputs of the CAR and CDR functions. That something can be
>> > passed to CAR does not make it a pair. Similarly, that something can
>> > be added with an integer does not make it an integer, or that
>> > something can be printed does not make it a string of text.
>>
>> R^5RS:
>> procedure: (car pair)
>>
>> Returns the contents of the car field of pair.
>>
>> + is a function defined to take parameters of type number, car (and cdr)
>> are defined to take a parameter (and not three parameters or ten or ...)
>> of type pair (and not string or ...)
>
> In Scheme yes; but you see other languages have a CAR function too.
>
> (car nil), or in Scheme terms (car '()), is exactly like (sqrt -1).
> You can restrict the domain to real numbers, and throw an error. Or
> you can extend the domain to the complex plane and return the
> imaginary value i.
It is not exactly like (sqrt -1) it is exactly like having the car
of a string return the first character.
>
> The Lisp version of CAR extends the domain from conses to lists, which
> include nil in the domain value, and introduces the rule that you can
> access the CAR and CDR (also known as FIRST and REST) of an empty
> list, producing NIL. There is nothing wrong with this extension of the
> list algebra.
see, the problem is that for this to be true or relevant you would need
to have a distinct list type (and not just a bunch of cons cells which
happened to link to each other). And consequently you are passing
judgment on what the base type should do based on something cobbled up
from the base types, a bunch of assumptions and a constant.
>
> People objected to imaginary numbers too when those were discovered;
> that's why they are called imaginary. The naysayers eventually had to
> cave in, because imaginary numbers turned out to be incredibly
> convenient, leading to straightforward computations and
> representations where contortions existed previously, for the sake of
> avoiding the computation of the square root of a negative number.
>
> In Lisp, we have an imaginary CAR field of the NIL value. It does the
> same thing as imaginary numbers: it takes some contortions out of
> expressions operating on lists, which exist only for the sake of
> avoiding the invocation of CAR or FIRST on an empty list.
Why, of course, and by being able to take the car of everything that
can possibly be broken apart and returning the object itself if you
can't will let you "take out of contortions of handling any object
as if it was a list". Its still a very bad idea though.
>
> Imagine that!
>
>> Strictly going by the text of R5RS its more correct to take the car of
>> a string than a car of '() and the first is not explicitly notes as
>> being an error.
>
> If you go strictly by the text of R5RS, then you can't think
> critically about the design decisions imposed by that text; you are
> within the box.
Why of course - then again, this is merely a lame thread trying to find
justifications from other languages to changing scheme. The decision
of limiting car and cdr strictly to operating on pairs is good regardless
of any practices of other languages to the contrary.
Why do you think languages like ML and Haskell chose not to use this
approach? Do you think the same reasons might apply to Scheme's choice?
That's a somewhat rhetorical question. I see quite a close connection.
Anton
> Otherwise you would have to define '() as the immutable pair
> which both "car" and "cdr" slots pointed to itself - imho not
> nice at all.
Why? I always thought that approach was rather elegant myself.
E.
Well, depends on whetever you want to treat '() as a simple constant
or a recursive structure.
>
> E.
Why do those have to be exclusive options? In Common Lisp it is both,
according as how you look at it. There is never any ambiguity unless you
wish to look at nil with the eyes of Pablo Picasso.
--
Erik Naggum, Oslo, Norway
Act from reason, and failure makes you rethink and study harder.
Act from faith, and failure makes you blame someone and push harder.
> Well, depends on whetever you want to treat '() as a simple constant
> or a recursive structure.
It can, of course, be both - a constant cons cell whose car and cdr
point to itself.
--tim
No it doesn't. It depends on what output you want from CAR in response
to what inputs, to obtain some desired level of convenience.
Common Lisp has (car nil) => nil without at all defining nil to be
some recursive structure.
Continuing with an earlier analogy, when mathematicians decided that
(sqrt -1) made sense, they did not need to redefine the integer -1 as
the area of some square whose length is (sqrt -1).
> No it doesn't. It depends on what output you want from CAR in response
> to what inputs, to obtain some desired level of convenience.
>
> Common Lisp has (car nil) => nil without at all defining nil to be
> some recursive structure.
But the structure just is whatever car and cdr return.
I don't care what the *implementation* of nil is. It's the
*semantics* which are recursive here.
In Smalltalk this corresponds to a method which throws an exception on
some inputs.
> That doesn't mean you can't handle such an error in your program,
> but the instant you decide to implement a special non-existence
> value and return that from a function, you're deviating
> philosophically from a pure functional approach.
I disagree. You are merely turning a partial function into a complete one.
The new function has a value where the old function had no value. In
Smalltalk terms, it returns nil instead of throwing an exception.
Complete functions are hardly contrary to the pure functional philosophy
:-)
> Returning "nil" as the answer to such a question, from the
> functional perspective, would mean that the answer to the question
> *is* known, and that it is "nil".
Agreed. But I see the difference as one of modelling. The new function
doesn't correspond exactly to the original question. There is a gap
between them. You can argue that the gap is a bad ideal from a modelling
point of view, but I don't think you can argue that it is less
"functional".
> One of the tricks of OO modeling is to figure out which things are
> static enough to be modeled as though they have identity, and how
> to divvy up the borderlines so that this works out.
Interesting idea. My first thought is that it confuses two kinds of
change.
The basic unit of state is the state variable. OO certainly combines
clusters of state variables into single entities, called objects. These
clusterings may or may not be stable. The arbitrariness you mention is
indeed involved.
However, this kind of stability issue would also arise even if there were
no mutable state. It is really an issue of modularity. For example, we
want to work with a Complex type, rather than a pair of Floats, because
doing so halves the number of entities we have to deal with. We gamble
that the pairing of one Float with another will be stable enough to make
it worth while. It has nothing to do with whether Complex has identity and
state.
And contrariwise, if we didn't have modules or objects, we could still
have state variables. Each variable would be atomic and so inherently
stable as a grouping, but it would still be mutable. Mutability is
orthogonal to granularity.
Objects are really about information hiding. For example, we might chose
to represent Complex in polar coordinates, as an angle and a length. Some
routines, such as those which implement addition and multiplication, would
need to understand this representation. Other routines can and should
treat them as a black-box, an abstract data type. This is true whether
addition modifies the Complex in-place or returns a new Complex.
> To summarize, objects are in fact an enormous hack for modeling the
> effects of state change over time in a particular way.
I disagree. I see them as a tool for encapsulation and information hiding.
State change is merely one of the things which might need encapsulating.
I don't see OO and FP as being opposed.
Dave Harris, Nottingham, UK | "Weave a circle round him thrice,
bran...@cix.co.uk | And close your eyes with holy dread,
| For he on honey dew hath fed
http://www.bhresearch.co.uk/ | And drunk the milk of Paradise."
You're right. See below.
> > Returning "nil" as the answer to such a question, from the
> > functional perspective, would mean that the answer to the question
> > *is* known, and that it is "nil".
>
> Agreed. But I see the difference as one of modelling. The new function
> doesn't correspond exactly to the original question. There is a gap
> between them. You can argue that the gap is a bad ideal from a modelling
> point of view, but I don't think you can argue that it is less
> "functional".
Also true, although the modeling issue goes to functional design in the
sense that you may have to contaminate an otherwise perfectly consistent
codomain (e.g. one consisting only of Addresses) in order to introduce a
non-existence value as a valid result.
You're correct that this is a question of modelling. I naturally tend
towards absolutism when trying to explain concepts like this, as opposed to
debating them. If the recipient can recognize finer distinctions, great.
> > To summarize, objects are in fact an enormous hack for modeling the
> > effects of state change over time in a particular way.
>
> I disagree. I see them as a tool for encapsulation and information hiding.
> State change is merely one of the things which might need encapsulating.
Once again, you're right. What I was really getting at is that in
encapsulating state change the way that they do, objects do something that's
much more questionable than other kinds of encapsulating that they do.
> Objects are really about information hiding.
Types or interfaces are about information hiding, too.
> I don't see OO and FP as being opposed.
Well, there's no strong definition of OO - it's a set of loose, adaptable,
and even unrelated concepts, many of the most important of which have
parallels in FP. OO can certainly coexist with FP under the right
conditions, but it seems to me that the tenets of OO that have any kind of
formal basis tend to actually be FP tenets, so I come close to agreeing with
David Rush's comment (iirc) that FP can subsume OO. However, a properly
FP'd OO doesn't quite look like the OO that most OO practitioners are
familiar with.
Anton
Actually I think that Anton was probably referring more to
domain-modelling than to the issue of objects in general. 'Utility'
objects (e.g. containers, cartesian numbers, &cet) are sufficiently
well-understood as to almost be a non-issue (except of course if you
don't have them).
> Objects are really about information hiding.
I don't actually think so. Types are about information *abstraction*
(a better term IMHO). Every definition of OO-ness centers on
trying to impose structure on the types involved in a program. At that
level you can still be an OO practitioner in any language (even
assembler). I've just found that the FP languages generally give you
better and more flexible tools for managing the type sturcture of a
program.
> > To summarize, objects are in fact an enormous hack for modeling the
> > effects of state change over time in a particular way.
>
> I disagree. I see them as a tool for encapsulation and information hiding.
> State change is merely one of the things which might need encapsulating.
Actually I agree with both of you. I got into the FP world because I
was looking for mathematical foundations for OO work. There are many
good practices and design habits to be gained from the OO world *and*
it has been a gentle introduction to types for many programmers, but
on the whole I find that OO practitioners have very little
understanding of the structure behind the cookbook approaches (of
which the design patterns movement is the latest manifestation) that
are widely advocated.
To summarize the summary: OO is an enormous (and effective) hack on
type management.
> I don't see OO and FP as being opposed.
Not at all, and Smalltalk is one of the best realizations of OO
principles as a programming language. I personally think that everyone
who has had their mind crippled by languages with broken type systems
should spend at least a couple of years programming Smalltalk. Besides
being great fun (Smalltalkers nearly invented the IDE - as distinct
from a REPL although I acknowledge the similarities), there are good
habits that arise from being relentlessly tied into the object model
(with an especially well-factored library right at hand).
And Smalltalk perfectly demonstrates the key element of
type compatibilty: behavioral compatibility. In spite of the fact that
Smalltalk appears to impose single-inheritance single-rooted
subtyping, the Smalltalk notion of type is actually entirely specified
by object interfaces because the *only* type check in the system is
the #notUnderstood: message.
For myself though, I'll take Scheme closures and SML functors as
better concrete realizations of behavioral typing and code re-use
because they also free the type structure of a program from the
restrictions of the single-rooted tree in Smalltalk. FP is OO with the
training wheels taken off...
david rush
--
There's man all over for you, blaming on his boots the faults of his feet.
-- Samuel Becket (Waiting For Godot)
> ... I don't see OO and FP as being opposed.
OO says we need objects who manage their own state.
FP says we should only have functions, and the value of
a function should always be the same for the same arguments,
i.e. the function should be stateless.
How do you have both in the same programming language?
Do you have functions with state, or objects without
state or what? How do you program an object that has
state, with a set of stateless functions? (Do you need
a 'main() program' to do that?)
Just curious and eager to learn .
Thanks
-Panu Viljamaa
That's one popular definition. If you insist that OO means mutable state,
then it is opposed to FP. However, to me a program has the OO feel even if
some of its objects are not mutable. According to this view, an object is
an instance of a class, and a class is an implementation of an abstract
data type, and mutability is not required. OO says we need objects who
manage their own /representation/, not state.
> Do you have functions with state, or objects without
> state or what?
Objects without state but with representions. Representations are
inherited, encapsulated, hidden from outsiders, message dispatch is
polymorphic etc - all the usual stuff of OO except mutable state. You use
messages like #copyWith: which returns a new collection, rather than
#at:put: which modifies in place.
No. FP insists that you use mathematically well-formed concepts. There
are at least three models of state mutation in the literature, of
which only one seems shaky to me (most likely I don't adequately
understand the theoretical basis), so mutation is a non-issue for many
functional programmers.
Note that there is a *large* subset of FP which does advocate the view
you hold, and the typically use non-strict languages which hide a
certain amount of state from the programmer and provide different
tools for managing the mutations that can't be hid. It might be better
to call these languages 'algebraic' FP because of their approach to
state, but that's just meself having opinions again ;) They call
themselves 'pure' FP.
> How do you have both in the same programming language?
Ask this question in comp.lang.functional, the Haskell programmers
over there do it all the time.
> How do you program an object that has
> state, with a set of stateless functions?
Monads.
david rush
--
Finding a needle in a haystack is a lot easier if you burn down the
haystack and scan the ashes with a metal detector.
-- the Silicon Valley Tarot
If you stick to the pure definition of a function always being the same for
the same arguments, then one way to model state is by generating a new
function when you want new behavior. The Scheme example I posted earlier
today, of an account function which maintains a transaction history, does
this: every time you specify a new transaction, you get a new procedure back
which includes knowledge of the latest transaction.
Another way to model state is by passing data structures around; it amounts
to the same thing, really. Either way, to avoid mutation, you create new
structures rather than changing old ones.
Regarding your characterization of these functions as stateless, while not
strictly incorrect, I think it can lead to some confusion. The behavior of
a given function forms part of your system's overall state. Just because a
function is in some sense stateless, doesn't mean a system composed of such
functions is stateless. Actually, the crucial issue is whether new
functions can be created - if a system creates new functions during its
operation, then the system as a whole is not stateless.
> Do you have functions with state
You can have functions with mutable state in the non-pure FP languages
(which is most of them, with a few exceptions like Haskell and Clean). This
is very common. As I've said, pure FP may be more useful as a learning
exercise, to get past the exact misconceptions about mutation and state
which you are currently experiencing.
> or objects without state
...which I'll recharacterize to "objects whose state does not change". You
can have those too - the Scheme example I posted essentially implements a
mutation-free object, using procedures.
> How do you program an object that has state, with a set of stateless
functions?
You make new objects, when you need them.
You need to separate the notion of object identity from issues of state and
mutation. A pure functional system can model a mutating object system
simply by creating a new object whenever one or more instance variable
values need to be updated. However, in object-think, you would consider
that new object to have the same "identity" as its predecessor. If your
objects have an ID field, then you now have two objects with the same ID
(and if you plan to keep them both around, this will have to be managed.)
If that seems inconsistent, think for a second about why it is the case:
each object represents the state of that entity at a particular moment.
They really *are* different objects - if, in real life, you could go back in
time and pluck an earlier version of a recently mutated object out of the
timestream, and set it down next to the current version of that object, you
would see that the two objects are different. They might share one or more
crucial attributes, such as the essentially artificial notion of "identity",
but those are just attributes. The two objects are no more the same object,
than you are the same person you were 10 years ago. Similarly, the future
Panu who understands the stateless state[*] of functional programming, will
not be the same Panu I am writing to today.
Anton
> ... If you stick to the pure definition of a function always being the same
> for
> the same arguments, then one way to model state is by generating a new
> function when you want new behavior.
If the program changes, then surely this is a state change.
But who holds on to the changed program? The 'system'?
Whose's state changes when the program changes?
In OO such a situation would be quite naturally modeled and
understood as an object which holds on to a method/program.
When that program chahges, we say that the state of the object
holding on to it has changed.
It seems to me from explanations given so far (thanks everybody)
that there is a 'main program' in Haskell, which takes input,
which may then cause the program to behave differently in the
future when it receives its next inputs. The state is perhaps
'hidden' from the programmer but it is there.
Saying that it nevertheless is 'pure FP' sounds like cheating,
and doesn't really explain the significance of why a program
with one stateful component (the 'main'), would be conceptually
and qualitatively more 'pure' than say a program (or a *system*)
composed of TWO stateful components.
-Panu Viljamaa
You're mixing up different notions of state. Even in a system where all
functions are "stateless" in the sense you described, i.e. same output for
same input, there is state as the program is executing any given
incarnation - that's how the computer gets from an input to an output, by
moving through a sequence of states. The functions you're talking about are
stateless when looked at from the point of view of the total function, i.e.
for all possible inputs and outputs. But the state you currently seem to be
concerned with is actually the internal state of the functions during a
computation, which is a very different thing.
For example, when you invoke, say, a recursive factorial program, although
variables are never mutated within the scope of the function, on each
invocation of the function, the parameters and any local variables may have
different values, and thus the function is in a different state "internally"
than it will be on some other execution. There's a distinction between the
mathematical concept of a function, and the computer concept of a procedure
which, when considered with all possible inputs, is capable of duplicating
the behavior of a particular mathematical function.
The recursive factorial is a mutation-free function, but its internal state
changes on every iteration. Now scale that up to whatever level you like,
and you have your answer. No cheating involved.
So, one way to answer to your question is to say that you're being tricked
into thinking a program has state, because you're only giving it one
possible set of inputs and then interacting with the internal state of the
program as it works through its predefined behaviors based on the inputs
you've given it. The "state" you're perceiving is just the fact that you're
interacting with one possible execution path of the program, at a particular
point in time.
However, another way to answer it is to say that the kind of state you care
about when interacting with a program is different from the mathematical
statelessness of a function. You just produce a particular set of inputs
which are threaded through the computation, you're not dealing with the
function as a whole.
That's why I said in my previous message that the term "stateless" as
applied to functions as a whole may be confusing in this context. I think
at this point, you'd be much better off focusing on the micro-level
restriction, which is simply that in pure FP, the value of a variable cannot
be changed once it's been assigned, within a given scope (e.g. function or
object). If you're asking "surely some variable has to mutate somewhere in
order to maintain the system's state", the answer is no, it doesn't.
Two particular features help make it possible to achieve literally anything
(computable) with a functional program, without mutation: recursion, and
first-class functions, i.e. functions as values. When I said "one way to
model state is by generating a new function when you want new behavior" you
responded with "If the program changes, then surely this is a state change."
However, the program hasn't really changed its behavior in terms of the
mapping of inputs to outputs. These "generated" functions are simply
values, like any other values. The possible set of functions that can be
generated by some other function is just as restricted by its inputs as is
any other mutation-free function. However, functions as values have the
ability to parameterize the future computation in powerful ways.
I probably shouldn't have introduced first-class functions into the picture
this early, though. The same perception of state change can be achieved by
passing ordinary data structures through a computation. That's why I said
earlier that functions provided "one way" to model state.
> In OO such a situation would be quite naturally modeled and
> understood as an object which holds on to a method/program.
> When that program chahges, we say that the state of the object
> holding on to it has changed.
In pure FP, changes of this kind are modelled by calling a function with a
new value for a parameter, for example. Recursion can help here - on each
recursive invocation of a function, the parameters can have different
values, and with tail recursion, you can keep this up forever.
> Saying that it nevertheless is 'pure FP' sounds like cheating,
It's not. The FP restrictions hold, and unlike Peter Pan's fairies, they
don't fade away or lose their importance because you doubt them.
> and doesn't really explain the significance of why a program
> with one stateful component (the 'main'), would be conceptually
> and qualitatively more 'pure' than say a program (or a *system*)
> composed of TWO stateful components.
There isn't one stateful component, in the sense you're thinking of. There
are zero.
However, a partially mutation-free program is still more valuable from a
computational analysis perspective than a mutation-happy program. That's
why most functional languages support, but discourage, mutation. There are
certain things for which it can be convenient to have mutating state. These
things can be restricted in such a way that the "purity" of the program's
design is not significantly affected. Entire subsystems can still be
mutation-free, for example.
I think I've said something like this before, but I'll say it again: you're
never going to get this stuff by talking about it, especially since you're
preprogrammed with a bunch of ideas that don't apply to the FP context.
You're going to have to write some code and get a feel for it, and it's not
going to happen in a couple of days. Are you reading any books or websites,
or working through any tutorials on the subject?
Anton
> ... much better off focusing on the micro-level restriction, which is simply
> that in pure FP, the value of a variable cannot be changed once it's been
> assigned, within a given scope (e.g. function or object).
I agree that is a more productive focus, and makes things easier
to understand. Especially if we can provide examples of the bad
things that happen when variables are assigned to multiple times.
Then while FP can be proposed as a solution/cure for these
bad things, we may realize that other solutions exist as well.
Depending on the specific circumstances, FP might be the best
thing to do, but in other circumstances it might not. But I don't
think "Pure FP" is a good solution, because it implies that the
*only* solution is always and everywhere FP (else it wouldn't
be pure, would it).
> If you're asking "surely some variable has to mutate somewhere in
> order to maintain the system's state", the answer is no, it doesn't.
I agree: *within* the program no variable needs to change. But
between the executions, the program itself must change, because
it will behave differently the next time.
Your argument is probably: The program is still the same
because the external database that holds on to the changed
state is really not part of the program, but just another
*input* to it, like any other input. From a single user's
point of view the program/function has changed however,
because it now gives different outputs to the same inputs.
I do like the idea that the behavior of a system at any given
time could be described by a function. And that the change of
the system in time can also be described by a function.
The question is: How easy is it to write such a function
for large complex systems, with multiple input- and output-
channels, with multiple programmers creating the system
in parallel, or at different times.
What would be the benefit of re-creating such a 'system-change
function' everytime requirements change, when we can more
easily program it with independent objects (some FP) that
hold on to their own state, and can be independently developed
by separate programmers.
For example, we know that the state of the Internet changes
continually, because users submit web-forms, post to
newsgroups etc; these inputs cause the outputs they see
from the web to change, based on their own and others' inputs.
It's quite natural to see that it was possible for this system
to be developed incrementally - because nobody had to write a
Scheme function that describes how the state of the Internet
depends on the history of inputs given to it.
Question: If Internet was programmed with pure FP, how many
arguments should the top-level function take?
Thus the technical argument is: To create a Purely Functional
System, we must also describe how it changes over time, with
a function. While this seems easy to do (with recursive-
and higher order functions) for simple components, it also
seems an impossible - and unnecessary - task for a large system
composed of hundreds or thousands of independently developed
components.
In other words the argument is: 'Pure' FP loses its pureness
when you want to scale up.
-Panu Viljamaa
There is a similar stunt in Smalltalk
with an object that is like nil (is in fact subclass of UndefinedObject)
but accepts *any* message and quietly returns itself.
You can get an implementation of this from my website: look for "void".
This thing reduces many fairly complex algorithms to triviality
for the same reason as expressed above.
Steve
--
Steven T Abell
Software Designer
http://www.brising.com
In software, nothing is more concrete than a good abstraction.
Nice idea. In Objective-C, that's the standard behavior of nil (or null?
I don't what it's called there).
In statically typed languages, you need something like the Null Object
pattern...
Pascal
--
Pascal Costanza University of Bonn
mailto:cost...@web.de Institute of Computer Science III
http://www.pascalcostanza.de Römerstr. 164, D-53117 Bonn (Germany)
Definitely, and I said something to this effect earlier - to save time, I'll
repeat that:
> To me the importance of FP is that it's the area from which current
> formal understanding of computer languages derives - in many
> respects, the concepts are much more important than the actual
> languages. Once you understand the concepts, you are better
> informed to decide things like when mutation of state is appropriate
> and when it's bad; when the language you're using is preventing
> you from using a more powerful solution; when objects are a good
> solution vs. when functions and closures make sense; and when to
> choose static over dynamic typing.
>
> While learning, though, it's a good idea to try a pure approach. It's
> hard to fully understand new benefits if you allow yourself to continue
> doing things the old way, like C programmers using C++ as "a better C".
My concern about the things you've been saying is that you sound as though
you're convinced that you don't need to learn much more about pure FP,
because of limitations you currently *perceive* in pure FP. However, the
problem is that you perceive these limitations incorrectly, and unless you
learn more, you're never going to properly understand the issues involved.
A really good way to learn some more would be to learn to program, and write
real programs, in a functional language.
> I agree: *within* the program no variable needs to change. But
> between the executions, the program itself must change, because
> it will behave differently the next time.
No, the program itself doesn't need to change. Functional programs don't
typically involve self-modifying code. Various other techniques are used
instead. I referred to one in my previous message: a user can interact with
the same overall function at different points in its execution. If you
terminate the program and restart it every time, with the same input, then
yes, it'll always give the same answer. But there's no need to do that, and
it wouldn't be very useful if that's not what you want.
You touch on another way to change the apparent behavior of a program:
> Your argument is probably: The program is still the same
> because the external database that holds on to the changed
> state is really not part of the program, but just another
> *input* to it, like any other input. From a single user's
> point of view the program/function has changed however,
> because it now gives different outputs to the same inputs.
Yes, providing a database with different information is changing the input.
The fact that a user perceives this a certain way doesn't change what's
happening, and doesn't detract from the functional-ness of the program in
question. You seem to be looking for a way to say "Aha! That's not really
pure FP", but in fact that'll only be the case if you choose to set things
up that way.
If you chose to, you could implement a "purely functional database", in
which existing records are never updated, but new records are added. This
is not uncommon in accounting systems, for example, or systems which need to
support bi-temporal capabilities, which include some very large commercial
systems, so it's not as though this is an unrealistic requirement.
Another way to look at the case with an external database is that the
overall system is described by a function which persists beyond any single
run of a program that uses that database. In that perspective, the user's
perception of the program having changed behavior from one run to the next
is once again due to the fact that the user is interacting with a single
function at different points in its execution. In this case, that single
function is not embodied in a single program: it is a conceptual entity
which encompasses the database and the programs which use the database.
> The question is: How easy is it to write such a function
> for large complex systems, with multiple input- and output-
> channels, with multiple programmers creating the system
> in parallel, or at different times.
At least as easy, if not easier, than it is with many other approaches. Try
"Functional Programming in the Real World" for some examples:
http://www.research.avayalabs.com/user/wadler/realworld/.
> What would be the benefit of re-creating such a 'system-change
> function' everytime requirements change, when we can more
> easily program it with independent objects (some FP) that
> hold on to their own state, and can be independently developed
> by separate programmers.
You're making numerous unfounded assumptions. There's no connection between
objects that "hold onto their own state" and the ability to independently
develop using separate programmers. As with almost any system/paradigm,
what's important in that respect is that the interfaces between components
of the system be clearly defined. When it comes to rigorous definition of
interfaces, the typed functional languages have some strong advantages.
> Question: If Internet was programmed with pure FP, how many
> arguments should the top-level function take?
One argument would be fine - e.g., a URL. It sounds as though you're
imagining some (non-existent) requirement that would somehow make this very
complicated. No-one has to write the function which represents all the
users of the Internet, or all the hosts connected to it - that "function" is
also known as the real world, and it has already been implemented (although
there's some question as to who the original Author was); all you have to do
is write functions which respond to the inputs it generates.
> Thus the technical argument is: To create a Purely Functional
> System, we must also describe how it changes over time, with
> a function.
This is hardly any different than the case with any other computing language
paradigm. In the FP case, by avoiding mutation, functions (or procedures,
to make the distinction from mathematical functions) naturally capture how
things change over time, in a more explicit way than imperative systems do.
Each time a procedure is called, it is provided a set of arguments that
represent a state at that point in the computation, which corresponds to a
point in time. You don't have to do anything special to capture change over
time, compared to other systems; all you have to do is write functions which
respond appropriately to their inputs. It's really quite easy.
> In other words the argument is: 'Pure' FP loses its pureness
> when you want to scale up.
No, pure FP never loses it's pureness - the term "pure FP" would be rather
meaningless otherwise. However, you might choose to compromise and use
something less than pure FP. Sometimes, compromises might be made for
apparently good reasons. Being able to tell which cases are which is
useful, and can be important.
Once again, I'll repeat that the point about pure FP is not that you should
necessarily begin using it in all your programs. The point is that you
should understand it, because it provides conceptual, computational and
formal models that are more rigorous, well-defined and powerful than any
extant model of objects (other than functional models!) However, once you
understand "it", you might find that you use it far more extensively and
comprehensively than you expected. You'll also find that in the instances
that you do choose to use mutation of variable values, that you'll better
understand what you're doing, and why.
Anton
Scheme distinguishes False from Empty-List, and I like
being able to make the distinction. But I also like
the simplicity that's achievable by blurring them from
time to time.
So I've developed a couple of alternative syntactic
forms that do exactly what the native syntax of the
language does except for ignoring this difference.
I denote them by adding an 'n' (for nil) to the name
of the standard function.
so 'ifn' acts just like 'if' except it treats the empty
list as false. And 'condn' acts just like 'cond' except
that if one of its initial clauses returns the empty
list, condn does not execute the code following that
initial clause, just as though it had been an initial
condition that had returned false.
I like this solution because it allows me to easily
*choose* whether to distinguish False and Empty-list,
and where to distinguish them. The same sort of approach
is probably usable in most other functional languages.
Bear