Hacker News new | past | comments | ask | show | jobs | submit login

> The difference for the immutable collections is that methods which would mutate the collection, like 'push', 'set', 'unshift' or 'splice' instead return a new immutable collection.

I think this is an unfortunate design decision which should be reconsidered. Functional operations should have different names than side-effecting operations. In general, I think that while side-effecting operations are commonly verbs, functional operations should be nouns or prepositions.

Particularly in a language without static types, you want to be able to look at an unfamiliar piece of code and see pretty quickly what types it is using. The semantics of mutable and functional collections are similar enough that using the same names is going to be very confusing, particularly in code that uses both kinds of collection -- and such code will definitely exist.

It's important that the names convey the semantics of the operation. Java's 'BigInteger' is a good example of this being done wrong -- the addition operation is called 'add', for example, and I have read that some newbies call it without assigning the return value to something, expecting it to be a side-effecting operation. I think that if it were called 'plus', such an error would be much less likely. We're used to thinking of "a + b" as an expression that returns a value, rather than an object with state.

I understand that introducing new names has a cost: people have to learn them. But keeping the old names is going to drive users nuts. If you won't change the names altogether, at least append an "F" to them or something.

EDITED to add: if you want some ideas, check out FSet: http://www.ergy.com/FSet.html

This is great feedback and a decision that I didn't take lightly.

I ultimately decided that the mental cost of remembering a new API would outweigh the potential for accidental return value mis-management.

It's hard to make a decision like this sans-data, so I had to make a gut call. I'm really interested to hear feedback of issues encountered in-practice due to this. Of course, if I'm wrong about this (and there's always a reasonable chance I am!) then I would seriously consider changing the method names in a future major version.

But it's not the same API. There is a clear and plain specification on what each of those operations do, and the methods on these objects do not do them. I get what you're thinking about re-using what the programmer already knows but this only poisons the well by adding confusion to their existing knowledge. "Push adds a value to a list, oh wait, except it depends what type of list".

It is much easier to remember that "foo always does 'a'" and "bar always does 'b'" rather than "foo does 'a' for some things but does 'b' for others", it's why we create functions and objects with different behaviors instead of nesting lots of if statements.

New objects with new behaviors should use new language.

I think there are two opposing forces: the first, as you say, pushes us away from re-using the same names, because that can cause confusion; the second, however, pushes us to re-use existing knowledge through metaphor.

This second force is ubiquitous in natural language, but common also in programming language where operators like "+" and "[]" are re-used in different contexts without practical ambiguity, and with the benefit of transfer of knowledge through metaphor.

So I think your conclusion -- "New objects with new behaviors should use new language" -- is a bit too strong. On the other hand, in this particular case, the context is very close (array behavior) and the only difference is the immutablity, so confusion is a valid practical concern.

I agree with you, context is important here. I think you can "borrow" some of the metaphor with different but suggestive names or patterns. But you shouldn't reuse a well-known name if your implementation is not faithful to the original associations. I really think specific language/word choice is under-appreciated in programming.

You've clearly put a massive amount of work into this API, which I applaud.

One trick I use in FSet that you might want to copy is default values for maps. This is particularly handy when the range type of the map is another collection: you can make the default value be the appropriate kind of empty collection, making it unnecessary for code that accesses the map to check for a null value. In Java, for example:

  FMap<Foo, FSet<Bar>> m = new FHashMap.withDefault(FHashSet.emptyMap());
  // now I can do:
  for (Bar b : m.get(x)) ...
  // without worrying about whether 'm' contains an entry for 'x'.

Great feature.

Immutable.js supports something similar but at the access site:

    var m: Im.Map<string, Im.Set<string>> = Im.Map();
    console.log(m.get('foo')); // maybe undefined
    console.log(m.get('foo', Im.Set())); // never undefined
typescript (and soon, flow) checks that the second arg to .get() is a Value type. Flow has the concept of non-nullable types, which will eventually let us type the return value of `get(key: K): V?` differently from `get(key: K, otherwise: V): V`

How about just adding aliases to existing methods, e.g. plus() and add()?

I empathize with your argument that operations with significantly different behavior ought to have different names, although I think the convenience of using familiar method names might outweigh the confusion of new readers (depending on the size/scope/structure of your software project).

I'm less a fan of your argument that method names like "push" or "add" inherently imply that they mutate their caller. I see no reason for that to be the case, other than in the context of your first argument. I think that an "add" method that returns a new integer without mutating its caller accurately conveys the semantics of the operation just as much as an "add" method that mutates its caller.

That's silly. All of the methods are side-effect-free. It'd be useless and confusing to suffix some methods and not others.

Not to mention that if you see the assignment, you know that it's returning something, so you wouldn't (shouldn't) assume that the method is necessarily mutating the object. Plus I like the idea of moving towards immutable by default and calling methods that by name are explicit about mutation.

I wholeheartedly agree. Methods associated with mutation should raise exceptions. If errors aren't eagerly generated, assumptions about behaviour might be made. Not to mention this will make testing harder.

Readability is impacted as well. Building a mutable copy from an immutable should be explicit. These calls should have a very prominent call signature that is easily read, not skimmed over.

Very cool library, but I can sense lots of confusion and debugging resulting from accidental misuse.

Guava's immutables are pretty solid and serve as a great reference.

Good point. One solution to this is loud warnings in the IDE about "unused return value". But since this is javascript it could in many cases be hard for the IDE to know what type you are calling the method on. (And technically all functions in javascript return something, undefined, which by some people might be considered a feature)

Depending on other API designs these warnings could also drown in false positives because some functions BOTH mutate the object and return it and you very rarely store the return value somewhere else, it's just there "for convenience" when chaining calls, such as foobar.add(123).multiply(456).subtract(789). Worse offenders are those that just randomly return something for the sake of it, take memcpy/memmove/etc in C for example, it accepts destination as a parameter and it also returns the very same destination for no good reason, i have never found a reason to use that return value.

An api should be designed so that unused return values in the majority of cases can be considered an error, unless explicitly ignored by casting it to (void) or something, most cases of unused return values you find are just people that are too lazy to check return codes which is about as smart as wrapping every statement in try/catch with an empty catch block.

Not sure if I agree - it's more natural to have only immutable collections if taking a functional approach. Eg see scala or haskell. Does strong typing change that? It's very clear if you're whole code base has no side effects what is happening when and where.

> It's very clear if you're whole code base has no side effects what is happening when and where.

And how likely is that? It's great if you start with a purely functional project but in most projects you're starting with a existing codebase, and you don't always have the time to go back and change everything when you figure out a better way to do it.

So I don't think it would be that unusual for someone to want to use immutable objects alongside alongside "normal" mutable objects. But it would be harder to distinguish the two in code when the methods are all the same, particular when you don't have type information at hand.

I suppose ruby is the example where you have map! and map. I don't know - the java stream API is clear enough IMO. The Java8 approach was to allow mutating functions on collections and then have lazy-non mutative stream api. As a java8 dev, I don't think it's so hard to understand that map on a play promise is non mutative though - I'm not dropping requests for typesafe to change the api.

Agreed. Related: "Methods which return new arrays like slice or concat instead return new immutable collections" seems odd to me, at least for slice. Isn't one of the advantages of immutable arrays that you can operate on slices without copying because of the guarantee that the underlying representation won't change?

Slice is done in O(logN) with very little copying. This works via structural sharing with the original List.

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