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

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.



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.


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.


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.


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.


> 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.


> 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)?




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

Search: