"Humane" programmer interfaces

D

Dave Benjamin

There's been a lot of discussion lately regarding Ruby and the notion of a
"humane" interface to objects like arrays and maps, as opposed to
"minimalist" ones. I believe the article that started the debates was this
one by Martin Fowler:

http://www.developertesting.com/archives/month200512/20051218-HumaneInterfaceOfMinimalInterface.html

And this one was posted in response by Bruce Eckel:

http://www.artima.com/forums/flat.jsp?forum=106&thread=141312

When I see the term "Humane Interface", I immediately think of the book
"The Humane Interface" by the late Jef Raskin. Although this book mainly
focuses on GUI interaction, it is also about the human-computer interface
in general, and makes some points that apply to the programming experience
as well.

The irony, I find, is that Raskin's definition of a humane interface seems
to better fit what Fowler describes as "minimalist". Raskin argues that
two ideals in interface design are what he calls "modelessness" and
"monotony". The two terms essentially relate to causes and effects.

Modelessness means that the same cause produces the same effect. "Modes"
are the different states that an application might be in; for example, if
an app has a "novice" and "expert" mode, the user has to be consciously
aware of which mode they are in, and performing the same action in
novice mode may not produce the same result as if it were performed in
expert mode. This somewhat resembles the notions of referential
transparency, side-effects, global and shared state in programming.

The other side of the coin, "monotony", says that one effect should have,
ideally, only one related cause. In a nutshell, "there's only one way to
do it". Many of the arguments given in this newsgroup in favor of this
Python design principle use a similar rationale:

"I use the term monotonous, tongue partially in cheek, to describe an
interface having only one way to accomplish a task (See Appendix B;
Alzofon and Raskin 1985, p. 95). Monotony is the dual of modelessness in
an interface. In a modeless interface, a given user gesture has one and
only one result: Gesture g always results in action a. However, there is
nothing to prevent a second gesture, h, from also resulting in action a. A
monotonous interface is one in which any desired result has only one means
by which it may be invoked: Action a is invoked by gesture g and in no
other way. An interface that is completely modeless and monotonous has a
one-to-one correspondence between cause (commands) and effect (actions).
The more monotony an interface has for a given task space, the easier it
is for the user to develop automaticity, which, after all, is fostered by
not having to make decisions about what method to use."

Thus, when I see people claiming that Ruby's array interface is more
humane because it offers a "last" method as an alternative to the
equivalent "get" method with a last index, I have trouble reconciling
these two views. I especially have trouble with the idea that method
aliases (like "size" and "length" methods that do the same thing) are
somehow more humane. How is it an advantage that half the programmers
accomplish a given task with method A, and the other half use method B? It
doesn't seem much friendlier to me, since I have to look up these aliases
to make sure my assumption is correct that they are identical. Usually,
when I see two names for the same thing, I assume there's some (perhaps
not-so) subtle difference in their purpose.

On the other hand, absolute minimalism may not be the most user-friendly.
Python's strings support "startswith" and "endswith" methods, which could
easily be implemented--outside of the string class--in terms of slices.
However, I love having these methods available off of the string, not
in some library of string utilities, because they are so handy and so
often useful (and they save me from having to think about indexes and
offsets when, conceptually, they are not relevant to the problem at hand).

As an example of the minimalist viewpoint, I highly recommend reading
Bjarne Stroustrup's interview with Artima from a couple of years ago,
called "The C++ Style Sweet Spot":

http://www.artima.com/intv/goldilocks.html

In the article, he explains an approach to class design that I have found
invaluable: giving the class responsible for maintaining the invariant,
and moving utility methods that do not bear this responsibility into
utility libraries. If this approach were taken with Python's built-in data
types, surely a few methods would have to go elsewhere, but I think that
Python takes a pretty minimalist approach, and encourages the writing of
utility functions as opposed to making every useful tool "part of the
core". I think that, in contrast, Ruby goes a bit overboard.

Note that Stroustrup is willing to budge on this ideal for the sake of
efficiency: "you might have five or ten member functions that are there
because they are logically necessary, or for performance reasons".
Likewise, Python sometimes includes methods in the core classes that could
be implemented elsewhere, specifically for the sake of performance.
"dict.setdefault" comes to mind. Another, more debatable reason, is
convenience, and this is where most disagreement is found. What is
convenient to one person is added conceptual baggage for another. It is
difficult to strike a good balance between the two, but erring on the side
of minimalism is much easier to correct in the long term.

One last comment I'd like to make is regarding the notion that minimalist
and humane are mutually exclusive. This is a false dichotomy. According to
Webster:

Main Entry: minimalism
Pronunciation: 'mi-n&-m&-"li-z&m
Function: noun
1 : MINIMAL ART
2 : a style or technique (as in music, literature, or design) that is
characterized by extreme spareness and simplicity

Main Entry: humane
Pronunciation: hy-'mAn, y-
Function: adjective
Etymology: Middle English humain
1 : marked by compassion, sympathy, or consideration for humans or animals
2 : characterized by or tending to broad humanistic culture : HUMANISTIC
<humane studies>

Accepting both of these definitions as (potentially) ideal, we have humane
minimalism: a style or technique that is characterized by extreme
spareness and simplicity, marked by compassion and sympathy, in
consideration of humans. In a world of increasing software complexity,
this doesn't sound half bad to me.
 
K

Kent Johnson

Dave said:
There's been a lot of discussion lately regarding Ruby and the notion of
a "humane" interface to objects like arrays and maps, as opposed to
"minimalist" ones. I believe the article that started the debates was
this one by Martin Fowler:

http://www.developertesting.com/archives/month200512/20051218-HumaneInterfaceOfMinimalInterface.html


And this one was posted in response by Bruce Eckel:

http://www.artima.com/forums/flat.jsp?forum=106&thread=141312

It was actually Elliotte Rusty Harold's response to Martin Fowler that
kicked of the discussion. The first one is here, there are several
followups:
http://www.cafeaulait.org/oldnews/news2005December6.html
One last comment I'd like to make is regarding the notion that
minimalist and humane are mutually exclusive. This is a false dichotomy.
According to Webster:

Main Entry: minimalism
Pronunciation: 'mi-n&-m&-"li-z&m
Function: noun
1 : MINIMAL ART
2 : a style or technique (as in music, literature, or design) that is
characterized by extreme spareness and simplicity

Main Entry: humane
Pronunciation: hy-'mAn, y-
Function: adjective
Etymology: Middle English humain
1 : marked by compassion, sympathy, or consideration for humans or animals
2 : characterized by or tending to broad humanistic culture : HUMANISTIC
<humane studies>

Accepting both of these definitions as (potentially) ideal, we have
humane minimalism: a style or technique that is characterized by extreme
spareness and simplicity, marked by compassion and sympathy, in
consideration of humans. In a world of increasing software complexity,
this doesn't sound half bad to me.

ISTM that Python does better than Java or Ruby in finding a sweet spot
between awkward minimalism (Java) and
everything-including-the-kitchen-sink generalism (Ruby, at least in the
List example that is being used so much).

Access to the last element of a list is a great example. Java forces you
to use the awkward list.get(list.size() - 1). Ruby gives you the
specialized list.last. Python generalizes to a way to access any index
from the end, with list[-1] as a concise special case of a general facility.

I think this is one of the great strengths of Python - that those who
create the language have been able to discover very powerful, general
concepts and apply them broadly and consistently to solve a variety of
problems.

Kent
 
K

kpd

list[-1] maps very well to my mental concept of list. To me 'List'
brings to mind a bunch of things in a line. It's intuitive to count
forward or backward.

Ruby's 'last' doesn't map as well for me because I don't think of the
list as having an attribute of 'last.'

Java just annoys me because I can never remember if it is 'size',
'length' or something else. Similarly, Java's conversion operators -
string to int or back - are something I am *always* referring to as
they just don't fit my mental model.

I agree with Kent, it's so subjective, but I've also found that Python
hits the right level.
 
C

colinwb

Apologies if I'm misunderstanding some points in kpd's post, but:

http://www.rubycentral.com/ref/ref_c_array.html

[ ]
arr[anInteger] -> anObject or nil
arr[start, length] -> aSubArray or nil
arr[aRange] -> aSubArray or nil

Element Reference

Returns the element at index anInteger, or returns a subarray starting
at index start and continuing for length elements, or

returns a subarray specified by aRange. Negative indices count backward
from the end of the array (-1 is the last element).

Returns nil if any indices are out of range.

[ ]=
arr[anInteger] = anObject -> anObject
arr[start, length] = aSubArray -> aSubArray
arr[aRange] = aSubArray -> aSubArray


http://tryruby.hobix.com

Interactive ruby ready.
ck = Array [ 'C', 'K', 'S', 'vvg' ]
=> ["C", "K", "S", "vvg"]
puts ck.first, ck[0], '*', ck.last, ck[-1]
C C * vvg vvg
=> nil
puts ck[0..2], '*', ck[-4..-2]
C K S * C K S
=> nil
4 4
=> nil

(The print elements in these example were actually
printed on separate lines. I've put them on the
same line for clarity and to save email space.)
 
A

Andrew Durdin

puts ck.first, ck[0], '*', ck.last, ck[-1]

One of the points at issue (minimalism/monotony) relates to TOOWTDI,
which has implications for language/module design and for code
readability. Ruby supports negative indices in the same way as Python,
so why add .last as an alias for [-1]? It's just added mental baggage.
And if you think .last is significantly more readable or convenient
than [-1], why not add .secondlast ? And .first, and .second ? With
one simple, obvious, general interface, these special cases only add
unneeded complexity.

Non est ponenda pluritas sine necessitate -- William of Ockham.

Andrew
 
A

Alex Martelli

Andrew Durdin said:
puts ck.first, ck[0], '*', ck.last, ck[-1]

One of the points at issue (minimalism/monotony) relates to TOOWTDI,
which has implications for language/module design and for code
readability. Ruby supports negative indices in the same way as Python,
so why add .last as an alias for [-1]? It's just added mental baggage.

In this particular case, it can be opined that it's more readable to
code goo.last than goo[-1]. For an analogy, consider, in Python,
somestr.startswith('glab') as a more readable equivalent of
somestr[:4]=='glab' -- the "why add?" question is easily answered,
although it's of course possible to disagree with the specific decision
made in each and every single case in which the same functionality is
provided in two distinct ways.

Python has a LOT of cases in which you can get the same functionality in
distinct ways, and not all of them are judged to fall afoul of the
"there should ideally be only one way" principle. Operations that have
to do with slicing of sequences are a substantial class of this "added
mental baggage", and not just for strings' endswith and startswith
methods. Consider, for example:

del somelist[start:finish]
somelist[start:finish] = []

exactly identical semantics. Or:

somelist.extend(another)
somelist[len(somelist):] = another

or (for suitable values of i, only):

somelist.insert(i, blah)
somelist[i-1:i] = [blah]

or

acopy = somelist[:]
acopy = list(somelist)

In each case, one could argue that the named alternative (del, extend,
insert, list, ...) is more readable; but the slice-based alternative is
also provided because slicing is such a powerful and general tool, and
when you're computing the slice's boundaries at runtime it's precious.

So, I don't think it would violate the spirit of Python to have
somelist.last mean the same as somelist[-1], e.g.:

class listwithlast(list):
def getLast(self): return self[-1]
def setLast(self, value): self[-1] = value
last = property(getLast, setLast)

Such tradeoffs need to be decided case by case, and, in Python, they
have been -- e.g., there's no "somelist.getodd()" synonym for
somelist[::2], for example;-). I think the myriad specific decisions
and trade-offs may be disputed (and it's not impossible that a strong
enough case can be made for one more such "synonym", so that a PEP
proposing it might in theory be accepted for a future Python version),
but essentially only on a case by case basis (it's good to keep in mind
principles such as "all other things being equal it's better to have one
way than more than one", of course, while taking such decisions).

I do draw the line at foo.size being defined as identical to foo.length,
which I personally classify as a design error (I don't think Python is
exempt from design errors at this minute level, either -- e.g., the
method name 'setdefault' is murky, though I can't propose a better one,
and the existence of both pop and popitem in dictionaries appears to
cause more confusion than benefit, where it might have sufficed to make
pop's argument optional if one wanted popitem's functionality).
And if you think .last is significantly more readable or convenient
than [-1], why not add .secondlast ? And .first, and .second ? With
one simple, obvious, general interface, these special cases only add
unneeded complexity.

first and last make sense (though it's debatable whether they should
exist or not, it's not obvious that they're design errors, not at all);
second, penultimate, and other variations, would be gilding the lily.
Just like startswith and endswith may (debatably) make sense, but, say,
hasrightinthemiddle would be surely inappropriate;-)


Alex
 
D

Dennis Lee Bieber

code goo.last than goo[-1]. For an analogy, consider, in Python,
somestr.startswith('glab') as a more readable equivalent of
somestr[:4]=='glab' -- the "why add?" question is easily answered,

Using constants for the example does obscure things a bit...
Compare:

needle = raw_input("Data starts with?")
while True:
...
if somestr.startswith(needle):
...

to

needle = raw_input("Data starts with?")
while True:
...
if somestr[:len(needle)] == needle:
...

--
 
A

Alex Martelli

Dennis Lee Bieber said:
code goo.last than goo[-1]. For an analogy, consider, in Python,
somestr.startswith('glab') as a more readable equivalent of
somestr[:4]=='glab' -- the "why add?" question is easily answered,

Using constants for the example does obscure things a bit...

A little, yes, and I do agree that writing len('glab') rather than the
"magic number" constant 4 would make things clearer, as in your example:
if somestr[:len(needle)] == needle:

This is definitely more readable, and the same would apply if I tested
somestr[:len('glab')] == 'glab' -- the key point being that one knows
where that slice limit comes from, rather than having to guess.

But that doesn't really change the main point, that Python does offer
redundant ways to perform several tasks -- a general one (often based on
slicing) and a specialized one with a readable name -- as in this
example and several others I brought. Yet another: you could always use
the more general 'extend' method to add just one element, via
somelist.extend([element]), but the more readable synonym
somelist.append(element) is also offered (and becomes thereby "the
obvious way" to perform this common task). Having a '.last' property to
perform just the same task as a [-1] indexing is not a very different
concept -- I'm not saying it's obviously right, but criticizing it as
"obviously wrong" because of the [-1]-indexing equivalence is, IMHO, a
very misplaced viewpoint. Much like I prefer using list(alist) to make
a copy (more readable), rather than the concise idiom alist[:] based on
slicing, I might well prefer to use alist.last rather than alist[-1], if
I had a choice (which doesn't mean I'd rather have a choice -- I think
that Python made the right tradeoff here, as in many other cases, but by
a very thin margin in this one case, IMHO).

That some (not all) of the numerous "general idioms" for which Python
offers more readable synonyms may require (or at least be best expressed
with) some len(...) call is hardly a big sub-issue -- some do, like e.g.
L[len(L):]=... as the equivalent of L.extend(...), some don't, like e.g.
del L[a:b] as the equivalent of L[a:b]=[], and yet the "alternative
readable synonym" gets offered anyway.


Alex
 
A

Andrew Durdin

Dennis Lee Bieber said:
if somestr[:len(needle)] == needle:

This is definitely more readable, and the same would apply if I tested
somestr[:len('glab')] == 'glab' -- the key point being that one knows
where that slice limit comes from, rather than having to guess.

I don't think there'd be much argument that
somestr.startswith('glab') is significantly clearer (in action and
intent) than somestr[:len('glab')] == 'glab' -- even if you use a
variable instead of repeating the literal. So this is not an example
of a purely redundant way of comparing the start of a string, because
there is a significant gain in clarity. OTOH, that somestr.last is
*significantly* clearer in action or intent than somestr[-1] is
questionable. I don't believe that it is -- but I agree with you that
having it is not necessarily a design error (i.e. "obviously wrong").
But that doesn't really change the main point, that Python does offer
redundant ways to perform several tasks -- a general one (often based on
slicing) and a specialized one with a readable name

In many cases there can be subtle semantic differences separating the
"redundant" ways, which makes them non-equivalent.
Much like I prefer using list(alist) to make
a copy (more readable), rather than the concise idiom alist[:] based on
slicing,

These are identical only for the case where type(alist) == list. If
alist issome other sequence-like type, then the equivalence no longer
holds.
e.g.
del L[a:b] as the equivalent of L[a:b]=[], and yet the "alternative
readable synonym" gets offered anyway.

These too may not be equivalent, if the behaviour of __setitem__ and
__delitem__ differ for L.__class__; although I cannot think of a good
reason to not have them equivalent for any sequence-like object.

As I said before "With one simple, obvious, general interface, these
special cases only add unneeded complexity." If the general
interface is either not simple or not obvious for a task, then a
special interface is likely more suitable.

Andrew
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

Forum statistics

Threads
473,744
Messages
2,569,484
Members
44,903
Latest member
orderPeak8CBDGummies

Latest Threads

Top