News for hackers. Stuff that matters.
Re:Robpike/Lisp: Toy Lisp 1.5 interpreter in Go by klodolph on Wednesday June 17, 2020 @12:07PM (#23552998)
Interesting: https://github.com/robpike/lisp/blob/master/lisp1_5/parse.go...

    // Expr represents an arbitrary expression.
    type Expr struct {
        // An Expr is either an atom, with atom set, or a list, with car and cdr set.
        // Car and cdr can be nil (empty) even if atom is nil.
        car  *Expr
        atom *token
        cdr  *Expr
    }
Instead of using interface types for expressions, there is a simple expression structure type with fields that may or may not be set depending on what type of value it is.

I probably never would have done it that way, I would probably use an interface type instead. This way actually saves space in cons objects… in the above version, a cons is 24 bytes, and below it would be 32:

    type Expr interface { }
    type Cons struct { car, cdr Expr }
Not efficient compared to modern Lisp interpretations, but an interesting choice.
Re:Robpike/Lisp: Toy Lisp 1.5 interpreter in Go by byronr on Wednesday June 17, 2020 @05:46PM (#23557164)
I wrote a toy lisp 1.5 interpreter in Go a few years ago, and part of the joy was making the core of the interpreter mimic McCarthy's typography. This is my apply function:

  func apply(fn, x, a Addr) Addr {
          if atom(fn) == T {
                  switch fn {
                  case CAR:
                          return caar(x)
                  case CDR:
                          return cdar(x)
                  case CONS:
                          return cons(car(x), cadr(x))
                  case ATOM:
                          return atom(car(x))
                  case EQ:
                          return eq(car(x), cadr(x))
                  default:
                          return apply(eval(fn, a), x, a)
                  }
          }
          switch car(fn) {
          case LAMBDA:
                  return eval(caddr(fn), pairlis(cadr(fn), x, a))
          case LABEL:
                  return apply(caddr(fn), x, cons(cons(cadr(fn), caddr(fn)), a))
          }
          panic(errint("bad node: " + car(fn).String()))
  }
Re:Robpike/Lisp: Toy Lisp 1.5 interpreter in Go by kodablah on Wednesday June 17, 2020 @01:29PM (#23554123)
> I probably never would have done it that way, I would probably use an interface type instead

I think when you're all in the same package and accessing unexported fields you know about, this is much clearer than an interface. I often add unexported mutually exclusive fields that local code has advanced knowledge about to prevent unnecessary over-abstraction.

Re:Robpike/Lisp: Toy Lisp 1.5 interpreter in Go by kccqzy on Wednesday June 17, 2020 @02:08PM (#23554622)
And yeah, and then a new team member doesn't know certain fields are mutually exclusive, and neither does the compiler because of the lack of sum types.
Re:Robpike/Lisp: Toy Lisp 1.5 interpreter in Go by brandonbloom on Wednesday June 17, 2020 @02:42PM (#23555071)
I find this a fascinating topic, so forgive me for this reply which has turned in to a mini blog post of sorts...

Go embraces the common insight that you can encode a sum as a dependent product. That is, here, you have a discriminated union of atoms and cons cells, but the discriminant is not a specific tag value, as it would be in Haskell or ML or similar, but rather the nil/non-nil state of these fields. Of course, there is no static enforcement of this property, as there would be in contemporary languages with sum types, including Rust's enums.

The quintessential example of this in Go is result/error multi-value returns. The result is valid if the error is nil. If your goal is to save bits (which is rarely the case for Go programs), as you note, avoiding an explicit discriminant (such as an interface pointer) means that you're encoding the discriminant's information in to less space, utilizing the specific domain knowledge of mutual exclusion.

Stepping further from Go, this same principle is at play in Clojure, which favors open maps for information. In Clojure, multimethods can dispatch on arbitrary functions of values. This means that any field can easily be used as a discriminant. Now, saving bytes is certainly not the purpose of this in Clojure, but it's interesting to think about. Forcing dispatch in to a privileged discriminant tag means that data needs to be transformed/parsed in order to re-arrange information in to the tag for dispatch.

Alexis King wrote an excellent article about this sort of transformation: https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-va...

As excellent as that article is, I think the truth lies in some balance. Both parsing and validation are useful techniques. Contemporary typed languages tend to push you down a path towards parsing instead of validation, which is probably the way the pendulum should swing for many use cases. However, there is one language that stands in stark contrast: TypeScript. Because it needs to support existing JavaScript idioms, it has grown a type system powerful enough to enable reasonable type safety with a validation based approach. Tools like control-flow sensitive type checking, literal types, and type-guards make that possible. You can use an arbitrary key in some JS object with a literal type value as a discriminant and the type system will do the right thing with union types.

There is one other area where Go is interesting here: zero values. Go zero-initializes all freshly allocated memory and encourages a style that embraces that. These freshly allocated objects are called "zero values" and often they are useful right out of the gate. You're encouraged to make code no-op safely with zero values, or apply some sane defaults. This makes an important distinction between `EnableFoo bool` and `DisableFoo bool`. Looping zero times is a perfectly valid thing to do. Silently skipping nil values is a perfectly valid thing to do in some cases. Etc. Clojure is similar with nil punning. While not without it's downsides, this is an interesting point in the design space that seems totally ignored by theoreticians and totally under-appreciated by working programmers, even those who work with Go and Clojure regularly. I'd really like to see that change, as I've found my programs have generally improved as I make judicious use of these techniques.

Re:Robpike/Lisp: Toy Lisp 1.5 interpreter in Go by setpatchaddress on Wednesday June 17, 2020 @04:20PM (#23556244)
Objective-C zero-initializes ivars, and it's common to rely on this. The big problem is that you still need to test that code is doing the right thing when it encounters a nil state.

If you define away the zero states completely with sum types, code flow is completely accounted for at compile time, and entire categories of "oops, that shouldn't be nil right now" bugs simply don't exist. On Apple platforms, this is a major reason to use Swift instead of Objective-C.

I haven't used Go, only read some code occasionally; perhaps there's some other difference here that makes this a nonissue. But it sure looks like it has the same set of problems.

Re:Robpike/Lisp: Toy Lisp 1.5 interpreter in Go by brandonbloom on Wednesday June 17, 2020 @08:45PM (#23558592)
It depends on whether or not you view "oops, that shouldn't be nil right now" as a categorically different problem than "oops, this integer shouldn't be greater than 10 right now" problems. Or "oops, this array shouldn't have an odd number of elements right now" problems.

Yes, it's nice to have the type system catch problems. And yes, in the context of memory-unsafe languages null or wild pointers are a big problem. But I've found that you'd need a combinatoric explosion of data constructors and abstract interfaces to enforce the interesting invariants of my programs. A nil pointer (which panics at runtime with a good stack trace!) tends to be among the easiest problems I have to solve when my programs violate invariants.

I'm not arguing that sum-types are a bad idea. Only that dependent products (with or without enforcement, static or dynamic) are an under appreciated technique. Sum-types are really just one special case of dependent products.

Re:Robpike/Lisp: Toy Lisp 1.5 interpreter in Go by ithrow on Wednesday June 17, 2020 @06:53PM (#23557783)
Stepping further from Go, this same principle is at play in Clojure, which favors open maps for information.

Curious, know little Clojure but isn't "open objects" a Javascript thing too? What is special about Clojure here?

Re:Robpike/Lisp: Toy Lisp 1.5 interpreter in Go by brandonbloom on Wednesday June 17, 2020 @08:35PM (#23558540)
In the context of that statement, nothing.

I called out Clojure specifically because of the culture around this specific issue and because of the dispatch design of the multimethods that are present in the standard library and widely favored. Nothing stops you from making dispatch tables or simply switching on a key in JavaScript.

Re:Robpike/Lisp: Toy Lisp 1.5 interpreter in Go by jstimpfle on Wednesday June 17, 2020 @03:08PM (#23555425)
Also called ZII - Zero Is Initialization, contrasting with RAII.
Re:Robpike/Lisp: Toy Lisp 1.5 interpreter in Go by morelisp on Wednesday June 17, 2020 @01:20PM (#23554002)
See also: Nearly every time I see someone trying to use `oneof` in protobuf...