HN user

kilink

102 karma

[ my public key: https://keybase.io/kilink; my proof: https://keybase.io/kilink/sigs/L2bIjKsfXWgHQPYbsXCntTq3nM0DKnxZ_9R819KAA8k ]

Posts11
Comments45
View on HN

I think the advice is more relevant in the context of the specific language; it's not universal.

In python you can go from attribute access to using a property (getter/setter) without breaking anything.

The same is not true in a language like Java where obj.foo is always a direct field access distinct from calling a method like obj.getFoo(), so going from public fields to getters is not backwards compatible and can be painful.

...Or can you?

  import ctypes

  def tuple_setitem(t, index, item):
      obj = ctypes.py_object(t)
      item = ctypes.py_object(item)
      ref_count = ctypes.c_long.from_address(id(t))
      original_count = ref_count.value
      if original_count != 1:
          ref_count.value = 1
      ctypes.pythonapi.Py_IncRef(item)
      ctypes.pythonapi.PyTuple_SetItem(obj, ctypes.c_ssize_t(index), item)
      ref_count.value = original_count

  >>> x = (1, 2, 3,)
  >>> tuple_setitem(x, 1, "foo")
  >>> x
  (1, "foo", 3)
Disclaimer: for entertainment purposes only, do not try this at home!

I'm not sure what you mean by error values being dropped on the floor by default. In Go or languages with ADTs you would have to consciously ignore an exception. Languages where pattern matching is prevalent force you to deal with the exceptional case to unpack anything useful from an Either for instance.

And if the study is to be believed, checked exception are almost always dropped on the floor, just with more boilerplate and ceremony.

I was addressing your specific assertion with regard to encoding error conditions as reserved values in your return type's codomain, which I agree is ugly.

One benefit of not throwing an exception is that you can maintain referential transparency, which exceptions generally break. You can't substitute a function call with its value because throwing an exception will have different effects depending on the context, which consequently hinders the composability of such functions.

Exceptions are also generally not type-safe, e.g. the return type of a function tells you nothing about what exceptions it may throw. If you go the checked exception route like Java, you do get some degree of type safety, but you do so at the expense of higher-order functions, which can't reasonably be expected to know about the specific exception types its arguments may throw.

On the other hand, ADTs are composable. They work for functions that aren't defined for some inputs (maybe an input type that can't be constrained by the type system). They avoid bugs because they force the caller to deal with the exceptional case (unlike returning null, a sentinel value, or throwing a RuntimeException), but without the boilerplate of exceptions, particularly in languages with pattern matching. The caller gets to decide when, if, and how to handle the exceptional case.

The problem usually is: What is the error code if your return type is int? Sure, you can define all negative ints as errors, or you provide a reference which is set when an error is encountered and so on, but that ends up being just another type of "very ugly".

Many languages support multiple return values, algebraic data types, or box types, allowing you to return an error value out of band.

Things I dislike about Ion, having used it while at Amazon:

- IonValues are mutable by default. I saw bugs where cached IonValues were accidentally changed, which is easy to do: IonSequence.extract clears the sequence [1], adding an IonValue to a container mutates the value (!) [2], etc.

- IonValues are not thread-safe [3]. You can call makeReadOnly() to make them immutable, but then you'll be calling clone since doing anything useful (like adding it to a list) will need to mutate the value. While it says IonValues are not even thread-safe for reading, I believe this is not strictly true. There was an internal implementation that would lazily materialize values on read, but it doesn't look like it's included in the open source version.

- IonStruct can have multiple fields with the same name, which means it can't implement Map. I've never seen anyone use this (mis)feature in practice, and I don't know where it would be useful.

- Since IonStruct can't implement Map, you don't get the Java 8 default methods like forEach, getOrDefault, etc.

- IonStruct doesn't implement keySet, values, spliterator, or stream, and thus doesn't play well with the Java 8 Stream API.

- Calling get(fieldName) on an IonStruct returns null if the field isn't present. But the value might also be there and be null, so you end up having to do a null check AND call isNullValue(). I'm not convinced it's a worthwhile distinction, and would have preferred a single way of doing it. You can already call containsKey to check for the presence of a field.

- In practice most code that dealt with Ion was nearly as tedious and verbose as pulling values out of an old-school JSONObject. Every project seemed to have a slightly different IonUtils class for doing mundane things like pulling values out of structs, doing all the null checks, casting, etc. There was some kind of adapter for Jackson that would allow you to deserialize to a POJO, but it didn't seem like it was widely used.

[1] https://github.com/amznlabs/ion-java/blob/master/src/softwar...

[2] https://github.com/amznlabs/ion-java/blob/master/src/softwar...

[3] https://github.com/amznlabs/ion-java/blob/master/src/softwar...

That also wouldn't even compile. I also write in multiple languages, and for languages for which this is an issue I use a linter. This isn't even an issue in C if you compile everything with the appropriate flags (-Wall or -Wparentheses).

For me it's not that it's confusing, it's annoying to read because it indicates that the programmer didn't actually take the time to learn the language, and is instead writing it as C/PHP something else where expressions in if statements don't need to be explicitly boolean (the examples in the article are Java). If they wrote that, what else do they not understand about the language?

Java if statements only allow boolean expressions, so the thing that the Yoda condition is supposed to protect against isn't even possible. I think this is precisely what the author is talking about, as this style is a totally unnecessary holdover from C.

That may true for Go, a language which still does not have a great dependency management story. I'm not sure this proverb is universal though.

I like Clojure quite a bit more than Java, but some of these are unfair if you are comparing against modern Java.

For instance, the sorting example can be simplified:

    Comparator<User> userOrdering = Comparator.comparing(User::isSubscription).thenComparing(User::getName);
    // forward sort
    Collections.sort(users, userOrdering);
    // reverse sort
    Collections.sort(users, userOrdering.reversed());
A lot less verbose than it's made out to be.

Too bad you can't just call functional interfaces in Java 8, or you could just do something like this:

BiFunction<BigDecimal, BigDecimal, BigDecimal> add = BigDecimal::add; add(x, y);

Unfortunately you have to do:

add.apply(x, y);

Which is kind of cumbersome.

First: constructor calling is not readable because Java misses named parameters. It's fine when there are 2-3 dependencies, but not more.

If you have more than 2-3 dependencies, it might be time to refactor things, as your class may be doing too many things.

Second: circular dependency is not possible.

Circular dependencies are possible with constructor injection when using a DI framework (e.g., using a Provider in Guice [1], or the equivalent in Dagger).

[1] https://github.com/google/guice/wiki/CyclicDependencies

I'm surprised he didn't mention AutoValue [1] or Lombok (using @Data and @Builder) in his section about data objects and builders. I prefer AutoValue, but both approaches cut down on a lot of the boilerplate (and they also generate sensible equals(), hashCode(), toString(), etc).

It's also kind of a nitpick, but I disagree with his preference for Guava's Maps.newHashMap() vs new HashMap<>(). I'm pretty sure the only reason those static methods exist is because pre-Java 7 didn't have the diamond operator.

[1] https://github.com/google/auto/tree/master/value

I think the Map interface should have been composed of two interfaces, ReadableMap and WriteableMap. Then APIs could return ReadableMap, which would lack the mutator methods altogether.