Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

Sometimes I think I must be a humongous idiot because adding a "days" method to Fixnum smells like the stupidest, most counterproductive, cutesy bullshit to me, but some pretty smart folks think it's great (that's a compliment btw).

Stuff like this is the opposite of expressive to me because cute expressions that read like english scream "please take a moment to analyze this and make sure it does what it sounds like." I am instantly suspicious of code that is so expressive that you can just intuit out its purpose without thinking it through. Code is too subtle for treating it that way.



I think I agree with the thrust of what you are saying here. Just as English has rules with what you do with nouns and verbs etc, code has rules too that are different to English.

2.days().from_now()

says that we are performing the action 'days' on the object '2'. What could that possibly do? Well there're a bunch of options and none of them are obvious. It's weird and counter-intuitive. And then we go and perform the action 'from_now' on whatever the result of that was.

I don't mind 'fluent' style interfaces as long as they don't mess up the normal meanings of what objects and methods are, but sadly they usually do. Another classic sign of a bad interface is that with many of these fluent interfaces it's not obvious whether a particular thing should be a chained method call or argument, and if it's an argument, whether it should be got by calling a method, using a field or what.

Code should read like clear code. English should read like clear English. These are not the same things, and efforts to make them the same usually make a mess of both.


Code should read like clear code. English should read like clear English. These are not the same things, and efforts to make them the same usually make a mess of both.

This stinks of principle over pragmatism.

Using the sugar offered by expressions like this requires a bit of knowledge of the development environment. I accept that it's a bit of a different syntax from some other languages, but that doesn't make it unclear at all.

Speaking to Ruby (where ActiveSupport is from), we all know that it's a language that focuses on providing flexible syntax and "multiple ways of doing things", in contrast to some other languages which restrict flexibility to provide more consistently structured code. I don't think it's a bad thing at all to have these options available, and I often find my intentions are much clearer when writing, say:

  if post.created_at < 5.minutes.ago
as opposed to Python:

  if(post.created_at < (datetime.now() - timedelta(minutes=5)))
or Objective C:

  NSDate *now = [NSDate date];
  NSDateComponents *delta = [[NSDateComponents alloc] init];
  [delta setMinute:-5];
  NSDate *fiveMinutesAgo = [[NSCalendar currentCalendar] dateByAddingComponents:delta toDate:now options:0];
  if ([[post created_at] compare: fiveMinutesAgo] == NSOrderedAscending)
That's where the magic comes in, and it makes Ruby ideal for things like web templating, while perhaps making it less suitable for other uses.

TLDR: various constructs like this have their place, they're not inherently bad, but do require knowledge of the tools.


    if post.created_at < 5.minutes.ago
That reads like "if post was created less than five minutes ago", i.e. if the post is more recent than five minutes. However, if "5.minutes.ago" gives you a timestamp, as is reasonable, then that actually means "if post was created more than five minutes ago", i.e. the very opposite of what it would mean in English. Whoops!

A more sensible syntax might be:

    if post.creation_date < now - 5*minutes
"ago", "from_now", "since_now" etc. are especially despicable in a language where you can just use + and -.


I'd prefer:

    if post.editable?
to everything I've seen. There's a concept behind the if statement that should go in the model. I realize this is sort of off-topic, but it kind of negates the whole discussion since you can wrap up the cute/ugly and test it alone. :)


And what would the definition of `editable?` look like?

    fn editable(&self) -> bool {
        self.created_at < 5.minutes().ago()
    }
Yes, it's wrong. No amount of unit testing or isolation is going to change the fact that whenever you read code like that, your brain will come up with two conflicting meanings, because these cutesy methods make it look too much like English, and English has different rules.

English parse tree is: ((less than (five minutes)) ago), whereas code parse tree is (less than ((five minutes) ago)).


A capital idea, my friend! But I'll do you one better: why not just wrap up the entire program inside a function called "main"? Clearly that would make all syntax discussions moot, now and forever.


I know this is a little nitpicky, but your Python example has way more brackets than are strictly necessary.

    if post.created_at < datetime.now() - timedelta(minutes=5):
I agree with your argument in principle though. Just wanted to point out if you're trying to make a point about readability / ease of use with code examples, you should try as hard as possible to make each example optimized to avoid accidentally creating a straw man.

Also, one criticism of the Ruby version is the invisibility of the types involved. What type is returned by 5.minutes vs 5.minutes.ago? How do I know I can compare the second to a date attribute from ActiveRecord? The Python version is more verbose, but I think it's more clear that I'm using two datetime's and a timedelta.


In both cases you have to either remember a lot of details, play with it in the reply, or look at the docs as you are writing it. Once it is written the ruby code is far easier to read.


what am I missing...

  if post.create_at < 5.minutes.ago
  if post.created_at < minutes_ago(5)
  if ([post created_at] compare: [minutes_ago 5])


In Rust, you have to import a trait (in this case active_support::Period) into the file that uses 2.days(). It's not a global change.

If you see 2.days(), you know where to look for the definition, and the type system means that you can easily learn what 2.days() returns. In this case, 2.days() returns a `TimeChange` object (https://github.com/wycats/rust-activesupport/blob/master/dsl...).

`TimeChange` implements a `from_now` method here: https://github.com/wycats/rust-activesupport/blob/master/lib... which returns a `Time`.

This is a lot easier to reason about than global modifications to Fixnum in an untyped language.


Yes, I took the fact that this was not a global modification of a widely used data type as a given, since to do so would be crazy in any language.

And sure, you can always look up in the type system or (in a less well typed language) the documentation to see what it does. The point of writing clear code is that doing so should be a last resort and not something you force on developers in order to understand what's going on.

Readability is what we're talking about here, and TimeChange.days(2) or new TimeChange(2, Days) are both more readable options than 2.days() because a programmer knows what is happening without querying information that isn't written in front of them.


> And sure, you can always look up in the type system or (in a less well typed language) the documentation to see what it does. The point of writing clear code is that doing so should be a last resort and not something you force on developers in order to understand what's going on.

Well, all you have to do in Rust is to look at the imports, since you have to import the trait defining the method at the call site. Because Rust modules never span multiple files, and imports must be at the top of the block that they're used in, that import will always be before the call. There are also no global imports in Rust (at least, without using a deprecated feature flag), so the trait in question is always named explicitly.


> Yes, I took the fact that this was not a global modification of a widely used data type as a given, since to do so would be crazy in any language.

Yet this is what Ruby does, AFAIK.


Yet this is what Ruby does, AFAIK.

Numeric#days is an ActiveSupport method, not core Ruby. But yes, it does patch the behaviour into a core class.

This is a bit of a legacy of Ruby programming, and it's definitely bad to pollute the global namespace. However, I expect we'll see eventual use of Refinements (http://www.rubyinside.com/ruby-refinements-an-overview-of-a-...) now that they are available; this will allow classes to override core methods without affecting other classes out of scope.


Refinements do a little less than previously advertised three years ago, and as far as language features go I don't think they're worth the performance implications.


This must be a question of aesthetics because the 2.days().from_now() seems perfectly obvious to me. Maybe someone can help me understand why it seems confusing?


It's non-obvious to me what 2.days() returns, that's the problem.


I don't think that's a problem: avoiding features or fiddling with syntax in order to make the code more accessible to people who are not familiar with the language or its libraries isn't going to gain much in the long run. Languages and libraries should be geared towards long-term developer happiness, rather than a shallow learning curve.

Anyway, I know as close as possible to nothing about Rust. It seems blindingly obvious to me that `2.days()` returns some kind of duration, and `2.days().from_now()` returns some kind of time. So I've looked at the code, and that appears to be correct - `2.days()` returns a `TimeChange`, and `2.days().from_now()` returns a Time. I can't think of any other reasonable interpretation.


Yeah, you _had_ to look at the code to realize that; when it could have been

     new TimeChange.fromString("2 days from now")
Which would have been more helpful for actual development, not only for a dumbed-down syntax aiming for easier comprehension.


Perl's autobox is lexical so it is also not a global change. For eg.

  use 5.016;
  use warnings;

  {
      use autobox;
      use autobox::DateTime::Duration;

      say 2->days->ago;  # 2013-12-27T14:03:47 (stringified output of DateTime object)
  }

  say 2->days->ago;      # Can't call method "days".... (runtime error)


I call this phenomenon "language skeuomorphism". See this comment I posted some time ago: https://news.ycombinator.com/item?id=5056667


You're saying `code` but what you really mean is "Java" or your favorite OOP paradigm. If someone comes up with different rules for what `2.days()` means then they are valid rules (and we can discuss whether they provide benefits to the other ones).


Not every nonsense 'rule' that someone comes up with is 'valid'. Rules need to be consistent.

And yes, I was talking OOP since we were talking about ruby. Functional or logic programs also have their own rules that tell you how to understand them. If you want to create good programs you shouldn't mess with those basic rules in order to make the code look more like something it actually isn't.


> 2.days().from_now() > > says that we are performing the action 'days' on the object '2'. What could that possibly do?

It does what the language definition says it does: it tells the compiler to search through traits that were explicitly imported to find a method called "days" and calls that. (Since "int" is a built-in type, it has no methods except for those provided via traits that were imported, so traits are the only potential source of these methods.) If there is more than one matching method, Rust never guesses: it instead signals an error.

> I don't mind 'fluent' style interfaces as long as they don't mess up the normal meanings of what objects and methods are, but sadly they usually do.

In this case they don't. It falls out of what traits and methods already provide in Rust.


This comment makes no sense to me, you don't like how clear it is? Somehow the clarity causes you to distrust what is actually happening?

Every time I have to write something like

Calendar cal = Calendar.getCalendar();

cal.setDate(new Date());

cal.addDate(-2, Calendar.DAYS);

Date date = cal.getDate();

I want to claw my eyes out. Is this what you consider more clear, less cutesy, somehow more productive than just 2.days.ago ?


Years ago I got into an argument about these sorts of "beautiful" and "intuitive" APIs:

http://www.reddit.com/r/programming/comments/21pnl/just_say_...

I still do not have good answers to the questions I asked in that first comment.


One possible answer: static typing + autocompletion.

Autocompletion clearly show what you can and can't chain, but admittedly it helps a little less when the DSL asks for a parameter of some type and you don't know how to start building one. Perhaps one way to evaluate the learning curve of an API is to count how many non-standard types appear in the signatures of its methods, excluding the `this` parameter.


You're conflating different things here. `Date.today - DateTime.days(2)` would be just as clear as `2.days.ago` without muddling the semantics of integers.


From where is that?

This is how it works in C# (and .NET in general)

DateTime.Now.AddDays(-2)

Not wanting to overload integers with calendar functionality doesn't mean you have to make it so cumbersome.

Edit: typo


You can also do:

  DateTime.Now - TimeSpan.FromDays(2)


I feel the same way that hamburgler does. For me at least, you java calendar code looks horrible. I'm thinking something more like

    date.today() - timedelta(days=2)


Note that this syntax is perfectly achievable in Ruby (w/ActiveSupport) too. The following expressions are equivalent:

  Time.now - 2.days
  Time.now.advance(days: -2)


If you can get exactly the same benefit, then why pay the cost of overriding behavior of the integer type and dealing with hidden black magic that makes your program harder to reason about?


I'm not sure which example you're talking about, but there's no additional cost in the Rust example. And it's not hidden; it's in the trait definition, which is scoped.


How about this 2.days.from_last_update? What does it do? What is going on with "from_last_update"? Is it actually reaching out to a database to find the last updated value? Which database? How do you configure it?

All these questions and more are why you should question "english as a DSL" methods. Granted, there's a ton of great libraries out there which do this well. Take Unfiltered's handling of parsing out requests for pattern matching:

  case POST(Path(Seg("api" :: "calls" :: name :: Nil))) =>
this is "englishy" without having you scratch your head. I've also seen

  case POST(Path("/api/auth")) & Authenticate(user) =>
which then causes a DB access to happen in a configured implicit value. Now try to reason out why something is not hitting a URL or being denied when you first get into a project. Unless there's someone to explain it to you, it just happens like "magic."

TL;DR How much magic do you want to deal with?


Your example is a sort of strawman argument, probably constructed without you being aware of it. "2.days.from_now" is indeed not free of side-effects, however "now" is a function that depends on the computer's internal clock and is generally understood what it does, the clock being a sort of global state that mutates and is generally available, whereas "2.days.from_last_update" implies usage of local mutable state, passed implicitly and highly dependent on the context you're in.

Also, this example is simply wrong ...

    case POST(Path("/api/auth")) & Authenticate(user) =>
It's wrong, because this is a route matcher, so if the user is not authenticated, the server would throw an HTTP 404 Not Found status. HTTP 404 Not Found is not meant to be thrown in case the user is not authenticated. There are more relevant, more correct status codes for that, like 401 Unauthorized or 403 Forbidden, or heck, in case of classic websites, a 302 redirect to a login page is in order. So you're basically giving broken examples and calling them "magic", but the examples are obviously broken, the first one in terms of API design, the second one in terms of functionality.

I really do understand your hint. But the examples you're giving are bad because they rely on totally non-obvious mutable state that's passed implicitly. This is indeed "magic", however it has nothing to do with syntactic sugar.

Btw, in Scala 2.10, thanks to String interpolation, you can also construct route-matchers such as this on [1]:

    case POST(p"/api/calls/$name/") =>
Doesn't work by default, you really have to build your own stuff for this to work (see the linked example). Or if you want to throw in regular expressions, you can either do this:

    case POST(p"/api/calls/${Integer(id)}/") =>
Or you can have like a mini-DSL with regular expressions built-in:

    case POST(p"/api/calls/$id<\d+>/?") => // note the optional ending slash
Yeah, you can do this. And it's certainly much nicer to me, more flexible and you can also make it faster than pattern-matching on Lists, as in your Unfiltered example. Is this magic? I don't see a local, mutable and implicitly-passed context, so for me it doesn't qualify.

Also, whenever I see "2.days" or "4.kilobytes" in Scala code, it's certainly nicer than seeing "172800 /* 2 days in secs /" or "4096 / 4 kb */". Given that these are statically type-checked extension functions that do not modify the original Int type and that do not rely on some mutable local state passed implicitly, is this magic? My IDE doesn't think so.

[1] https://github.com/alexandru/shifter/blob/master/web-api/src...


I think it basically comes down to trust. Can you trust that the person who wrote the library isn't a dingbat, and that they've thought out the edge cases and composability of the interface they've designed, and that they've taken care that code actually does what it looks like it does, and confusing cases are either inexpressible or at least look confusing so you know you have to look closer. In short, can you trust the author to design interfaces well.

Personally I don't think it's inherently good or inherently bad, it's just one of those things that makes good code better (concise & expressive) and bad code worse (inscrutable & brittle). Glass-half-full and glass-half-empty are both valid opinions on the matter, and I strongly suspect which way you leans towards depends heavily on the quality of other people's architecture that you have to deal with on a daily basis.


> Can you trust that the person who wrote the library isn't a dingbat

Almost never.


"Let's just throw away the single responsibility principle and give Integer a method that multiplies it by the number of seconds in a day, and another one that adds it to the current day to get a new Time or Date object. Obviously that's something that an integer should be concerned with."


That's kind of the philosophy of Ruby though, and ActiveSupport by extension.

Where it makes code clearer and more concise, or where it simplifies an often-used idiom, and where it outweighs the benefit of sticking to principles—as I believe it does here—then breaking those principles is acceptable, in languages where coding conventions support this.


I don't think `2.days.ago` makes code any clearer or appreciably more concise than, for instance, `Date.today - Date.days(2)` (or even, to meet you in the middle, `Date.today - 2.days`).


We must not mistake principles and guidelines for absolute rules. I'm not judging this particular case, but I see the SOLID guidelines treated as absolutes far too often.



As I mentioned in the post,

> Now, I should say that I almost never use 2.days, but the point is that it’s a benchmark of expressiveness: if you can write it, you can do all sorts of other flexible things.

I tend to use MiniTest, not RSpec. But I'm very glad RSpec can be built.


I use https://github.com/sconover/wrong with MiniTest which might qualify as another example of Ruby "tricks" to achieve a different interpretation of expressiveness.

The result is the ability to express

    assert { :apple == :orange }
    assert { [:apple, :banana, :pear].include? :orange }
and let the computer figure out the appropriate error message and expected-vs-actual diff.


The folks who "'think' this is great" are the same folks who self-submit their blog entries to news.ycombinator.com: self-promoting quacks. Bournegol has been around for awhile:

http://oldhome.schmorp.de/marc/bournegol.html


I found this a lot less problematic in scala with IDE support. You can write 2.days, with appropriate imports, but the IDE will highlight the implicit conversion on the 2 (a green underline seems to be the standard visual representation), and you can click through to the definition.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: