News for hackers. Stuff that matters.
Re:Go and Simplicity Debt Redux by alkonaut on Sunday June 18, 2017 @05:46PM (#14583129)
> Everyone who wanted to do basic things like read input or write output would have to understand how option type, in addition to discussion of what templated types are

I don't necessarily think that's a bad thing. Something that may be null is already an option type. It's "some T or null". Making the compiler force you into checking which it is, is no difference from checking for the presence of null. There is literally no difference in complexity - it's actually simpler because you don't have the cognitive overhead of remembering where a certain variable has and hasn't been null checked (After a null check, your variable has magically changed type to "some T, definitely not null".)

   x = getsomething  
   if x != nil
     dosomething(x)
and

  x = getsomething
  if x.hasvalue
    dosomething(x.value)
The programmer alone has to remember this - remember to check, and the status of each variable that can be null. Was x checked before? scroll up to see... Add 10 variables and some nested ifs and ask the programmer which variables can and can't be null at any given place. This is the job of a compiler, not a programmer. And if my experience is any guide, the less experienced programmers, which are the ones we are discussing now, would be happy to have 100 locals or 12 levels of nested ifs in a method. They need this added help more than the people who ask for it.
Re:Go and Simplicity Debt Redux by hota_mazi on Monday June 19, 2017 @03:21AM (#14585127)
Note that these is absolutely zero gains with the second approach if the compiler doesn't enforce it.

And I very much don't envision that the Go compiler would be modified to make sure that `x.value` is never referenced unless the presence of that value has been confirmed by an earlier statement.

Re:Go and Simplicity Debt Redux by alkonaut on Monday June 19, 2017 @03:53AM (#14585237)
> Note that these is absolutely zero gains with the second approach if the compiler doesn't enforce it.

The compiler enforces it by the fact that you can't pass in the return value from one thing (that might not be a valid value) into the next function that takes an argument that must be a valid value. Your objection that "but I can unwrap/call value without checking so it's useless" is a commonly raised objection - but it's a false one.

With nulls

    var x = get_something 
    do_something(x)  // ok even if x is null

With option/maybe

    x = get_something
    do_something(x)       // compiler err
  
    x = get_something
    do_something(x.value) // ok, *concious action by developer*

    x = get_something
    if x.hasvalue
      do_something(x)     // ok
The difference in safety between having to make a concius unwrap IS the whole point of the feature. This works exactly the same everywhere. You can always ".unwrap()" in Rust for example. Having to unwrap, or unwrapping without checking first isn't wrong, in fact it's often right. Unwrapping the value first means "extract the value if it exists, or otherwise panic right here". This decision to "assume success or otherwise panic" is a clear intent that the next developer can read.
Re:Go and Simplicity Debt Redux by catnaroek on Monday June 19, 2017 @02:17AM (#14584905)
No, nulls aren't options. Options nest. You can have Some(Some(Some(foo))). The non-compositionality of null makes it impossible to abstract it away.
Re:Go and Simplicity Debt Redux by alkonaut on Monday June 19, 2017 @02:28AM (#14584934)
Yes I only meant in the sense of null adding a "maybe".

    x = fetch_an_int  // x is definitely an int
    y = fetch_an_obj  // y is *maybe* a T.
Re:Go and Simplicity Debt Redux by brandonbloom on Monday June 19, 2017 @02:27AM (#14584927)
Huh? What kind of "nesting" are you talking about? How is that different than nesting structs containing nullable pointers?
Re:Go and Simplicity Debt Redux by catnaroek on Monday June 19, 2017 @02:42AM (#14584972)
In ML, if you have a type “foo”, then you can also use the type “foo option option option” without creating any new nominal datatypes. In particular, using a value of type “foo option option option” is just as easy as using a value of type “foo” or “foo option”:

    case wrapped_foo of
        NONE => ...
      | SOME NONE => ...
      | SOME (SOME NONE) => ...
      | SOME (SOME (SOME foo)) => ...
On the other hand, with your proposed alternative, you have to create a wrapper struct for every nullable kind of thingy:

    type MaybeFoo struct { foo *Foo }
    type MaybeMaybeFoo struct { foo *MaybeFoo }
    type MaybeMaybeMaybeFoo struct { foo *MaybeMaybeFoo }
And, as if that weren't offensive enough, you have to manually pack those pointers into structs and then unpack them back.
Re:Go and Simplicity Debt Redux by brandonbloom on Monday June 19, 2017 @03:15AM (#14585103)
1) You're not "abstracting away" anything like that. If you're talking about operations you can abstract over `a option`, that has nothing to do with nesting.

2) I would consider explicitly nested option types to be a code smell. If `foo option option` happens incidentally via a module functor or something, fine, but if you ever write that by hand, you probably want to explicitly flatten that to a 3 or 4 case sum type with explicit constructor names.

3) Nested structures like this are very unlikely to have synthetic, abstract names like MaybeMaybeFoo. Instead they are more likely to have operational, domain-specific, concrete names names. These structs are likely to _already exist_, since Go lacks type inference for function parameters: You're already going to have to create types in order to talk about these things.

4) You're "manually" packing/unpacking by writing (SOME (SOME ... constructor calls and in your pattern match. The difference between &Foo{&Bar{...}} and (Foo (Bar ...)) is irrelevant. Meanwhile, pattern matching has a syntactic advantage over a `== nil` check, but again, I'd argue that intentional nesting of numerous expected absent values is a code smell. If you really need that, you'd probably employ the null object pattern, which is easy to support in Go because methods can be called on nil instances. I'll say: I've never had to do that in tens of thousands of lines of production Go.

Re:Go and Simplicity Debt Redux by catnaroek on Monday June 19, 2017 @03:19AM (#14585120)
1) When I make an abstract type “foo” whose underlying implementation (hidden to clients) is “bar option”, I'm abstracting over an option type.

2) What if, in client code, I want to make a value of type “foo option”, completely unaware that foo's underlying implementation is “bar option”? Is it still a code smell? Do I have to know what abstract types defined by other people are implemented as options, so that I don't wrap them in options of my own?

3) Meh.

4) My pattern matching is completely safe. OTOH, with your proposal, you can't safely unwrap those nested structs of nullable pointers in a single step, because you need to unwrap the first layer just to check whether the second layer is safe to unwrap.

---

2) The problem exists whether there exist non-disjoint types or not. Oftentimes I do want to use nested types, because that's the way the data is naturally structured, i.e., that's what shortens the description of the operations that act on the data. Flattening the nested type into a single sum with 4 constructors would merely impose on myself of undoing the flattening every time I want to operate on this sum.

3) It's abstraction for the sake of making code easier to reuse and verify.

Re:Go and Simplicity Debt Redux by brandonbloom on Monday June 19, 2017 @04:10AM (#14585296)
1 & 2) Read my comment again. My remark about functors (in the ML sense) addresses this point. I'm quite well versed in how type abstraction works. Your comment about being aware of underlying type details is completely non-sensical with respect to nesting. The problem you're worried about applies to non-disjoint union types, ie dynamic typing, not Go's typical use of type-safe pointers. Nil is type-safe in Go, there's no risk of confusing:

    a nil *Foo with a nil *Bar
3) You say "meh", but abstraction for the sake of abstraction is exactly what drives working programmers away from so called "better" languages.

4) Meh.

Anyway, I'm done with this thread.

Re:Go and Simplicity Debt Redux by alkonaut on Monday June 19, 2017 @02:51AM (#14585013)
The only type I'd make (if it wasn't in the standard library) is the regular Option<T>. If I have an instance of Option<T> and the method takes a T, then I'd have to check and unwrap it. I can't see the need for creating multiple structs assuming you can nest the Option<Option<T>> when required (and heap allocation is OK)? Perhaps I misunderstood something.

If I have an instance of Option<Option<T>> then I'd have to unwrap it more than once. However, if the language has no concept of such automatic unwrapping, the chance that you would ever end up with a nested option is pretty slim. Basically, the monadic type use of these is an effect of the language doing that, not the other way. In e.g. C# where I use option types extensively I'm yet to see a wrapped one. In F# it's different.

In C# there is a feature in which nullables (e.g. int? and int?? or int???) actually work similarly because there are "lifted operators" which sort of half-achieve this, e.g.

   int? x = 10;
   int? y = 20;
   int? z = x + y;
but to be honest this to me feels mostly quirky because it doesn't extend to much else in the language.
Re:Go and Simplicity Debt Redux by catnaroek on Monday June 19, 2017 @03:06AM (#14585072)
> Basically, the monadic type use of these is an effect of the language doing that, not the other way. In e.g. C# where I use option types extensively I'm yet to see a wrapped one.

And C# pays dearly for it. Why doesn't C# have anything like C++'s <algorithm>, or Rust's composable iterators, or Haskell's plethora of composable abstractions (foldables, traversables, lenses, etc.)?

> In C# there is a feature in which (...) but to be honest this to me feels mostly quirky because it doesn't extend to much else in the language.

Agreed. Hardcoded special cases are always a bad idea.

---

Sorry, HN won't allow me to make a new post because “I'm submitting too fast”, so here goes my reply:

C++ has a finer-grained hierarchy of iterator concepts:

(0) InputIterators correspond to C# IEnumerators.

(1) OutputIterators allow you to write to the sequence's current element.

(2) ForwardIterators allow you to dereference the current element arbitrarily many times before moving on to the next.

(3) BidirectionalIterators allow you to move both forward and backwards in the sequence of elements.

(4) RandomAccessIterators allow you to skip arbitrarily many positions in the sequence of elements in constant time.

And there are algorithms defined in terms of these refined iterator concepts.

Re:Go and Simplicity Debt Redux by alkonaut on Monday June 19, 2017 @03:28AM (#14585152)
> Why doesn't C# have anything like C++'s <algorithm>, or Rust's composable iterators, or Haskell's plethora of composable abstractions (foldables, traversables, lenses, etc.)?

I'm not very familiar with the details of Rust iterators yet, but there isn't much I'm missing from Linq at least yet (apart from the cost control available in Rust and C++ that it's pretty natural is omitted from C# where it's natural that operations such as iteration can heap allocate).

What is the difference between <algorithms> and the similar Linq (or rather the enumerable extensions enabling linq)?