Ask HN: Great programming language features, other languages should steal?

25 points by manx ↗ HN
You cannot be fluent in all programming languages. But this also makes you blind for language features a particular language really got right, or killer libraries that are just amazing. Which are these widely unknown features, that you only get to know, when working with a language and its ecosystem? Example: I'm coming from Scala. Starting to develop a Flutter app with Dart, made me realize how great and safe the serialization ecosystem in Scala is. You can serialize types (and type hierarchies) with a convenient syntax that is completely decoupled from the type itself, while high-performance serialization code is generated at compile-time via macros. Libraries include: https://circe.github.io/circe/ https://github.com/suzaku-io/boopickle

43 comments

[ 4.1 ms ] story [ 112 ms ] thread
I really like the trait/struct/enum model of programming that you find in Rust. I'm a quant and I do a lot of higher math, and the trait method of programming is very very similar to how you might construct algebraic structure in the set=>group=>field etc way.
JavaScript’s object destructuring
My ideal language is something like F# but with HKTs, trait, macros, union types (without discriminator), basically most features dotty aka scala 3 introduced.
Its funny though, as I am personally switching to scala 3 for the features I mentioned. I find the features particularly handy for building distributed systems using Akka. And I wished they'd get tooling just right out of the bat. (sbt doesn't seem great)
1. Python's list slicing and indexing is the perfect mix of simplicity, intuition and power. It is surprising how few languages have similar implementations.

2. Currying like in Haskell.

Python list/set/dict comprehensions too
And generator comprehensions -- it's especially nice that you can directly write things like `sum(x*x for x in g)` without needing an extra set of parentheses or allocating an intermediate collection.
1. Pattern matching with destructuring a la scala, rust, ocaml, etc.

2. scoped threading a la kotlin and by using certain libraries, rust to a degree.

3. structs / records (i feel bad putting this since every language but java seems to have it).

4. macros of some kind

5. type inference

6. enums a la rust / sealed types in kotlin which are needed for pattern matching afaik

R6rs scheme's (or even better Racket's) macro system. Recently similar systems have been making their way into other languages, but the AST-like syntax of lisps make it a fair bit easier to use.

Other than that? Delimited continuations. Guile-fibers, a parallel concurrent ML (which I would describe as a generalisation of go's concurrency model) for guile scheme is implemented using them as a scheme-only library. No low level voodoo. Just straight forward scheme.

* Pattern matching with totality checking. Warn us if a cased is missed.

* Higher-kinded types. Can a List<Int> and a Stream<Int> both be considered a T<Int> ?

* Differentiate between functions with and without effects.

I like how Erlang handles binary and bitstring data. Extremely useful if you're doing any sort of protocol implementation.
So many programs I've written would have been easier with Erlang's binaries and pattern matching. A lot of what I've done has boiled down to parsing and transforming binaries from one format to another. I've made a few POCs for work in the past (that I couldn't have convinced them to use no matter what, only C/C++/C#/Python were used) that demonstrated a much simpler version of core program logic because of those two features. The code was often 1/10th the size (if not smaller), clearer, and more extendable. And this wasn't just because I was writing it for a second (or third) time. The code in the other languages was good, clean code as well. It's just harder in those languages to express the same things as concisely.
1. I like Erlang's Supervisor. If Go's goroutine has a unique ID, I think it can steal that design.

2. There was one new language that I don't remember the name, its data structures automatically turned to thread-safe upon concurrent access. That's smart. I wish all languages do that. Less data structure names people have to remember.

3. The compiler can cross-compiles to many different architectures similar to Go or Zig. This makes deployment story much simpler.

4. The compiler produces single binary by default. This also makes deployment story simpler.

5. This is year 2020. Every language must have an event loop so that they can have async I/O routines. Preferably in the form of CSP (green threads + channels).

As you can see I care about the ergonomics and tooling outside the language itself.

This is pretty minor but I like how in Rust you can use constructs like if-statements and match-statements as expressions. For example:

  let foo = if some_condition {
      "condition is true"
  } else {
      "condition is false"
  };

  let bar = match x {
      0 => "string",
      1 => "another string",
      _ => "default"
  };
It's not super significant but I think it's an elegant syntactic feature.
They got that from lisp.

But in lisp it's more than just a syntax trick that only works for a few predefined cases.

And Algol and others. It's a nice feature when available, certainly my preference. I find I can often write things more cleanly given an expression oriented languages than ones dominated by statements.
(comment deleted)
You could do this in GNU C probably at least as far back as 1990. In GNU C, this syntax:

  ({ expr1; expr2; ... ; exprN })
gives you something like a Lisp progn: it's an expression, whose value is exprN.

Complete sample:

  #include <stdio.h>
  
  int main(int argc, char **argv)
  {
    char *foo = (argc > 1) ? ({
                  printf("consequent\n");
                  "condition is true: I have arguments";
                }) : ({
                  printf("alternative\n");
                  "condition is false: I don't have arguments";
                });
    puts(foo);
  }
This is almost certainly because RMS is a Lisp hacker. I.e. why GCC uses Lisp notations internally and functions with "mapcar" in the name and such.

I discovered it by accident sometime in 1993. I wanted the value of the last expression in a braced block. Maybe it can forced into an expression if you just put parentheses on it? By golly, the intuition worked.

Mainly, it is used in writing macros. Speaking of which:

  #include <stdio.h>

  #define ifblock(expr) (expr) ? (
  #define elseblock ) : (
  #define endblock )

  int main(int argc, char **argv)
  {
    char *foo = ifblock (argc > 1) {
                  printf("consequent\n");
                  "condition is true: I have arguments";
                } elseblock {
                  printf("alternative\n");
                  "condition is false: I don't have arguments";
                } endblock;
    puts(foo);
  }
Kotlin, too!
Golang, avoid this idea in the service of software engineering.

Go tries to be a syntax minimalist so that the writers can make what's there the best rather than building the world of fancy new PL features. It also means there are fewer days of doing the same thing and most people's code looks the same. <- this is such a difference maker

Python data structure manipulation/comprehensions. I realize there's some overlap with other languages, but this to me is Python's 'killer' feature.
Goal-directed evaluation from Icon. Makes generators composable.
It's more of a paradigm than a feature but, multiple dispatch à la Julia. One of the creators of Julia makes a strong case that it is the key to libraries that "compose" without being aware of one another:

https://www.youtube.com/watch?v=kc9HwsxE1OY

They got that from lisp.
Julia compensates for time lost by making a lot more use of it in the stdlib than, say, most CL implementations do. It is as if the julia people saw it and went "wow, this is good!" and had it in from the beginning. With their focus on maths, it is hardly surprising.
There's actually a pretty fundamental reason that Julia's Base and stdlib makes more use of multiple dispatch than Common Lisp (or any other language in existence with multiple dispatch). It's that in basically every other language, multiple dispatch comes with a non-neglible performance penalty.

Julia's multiple dispatch semantics were designed around having a JIT compiler, and it's JIT compiler design was designed around having multiple dispatch, and this tight integration let us be really good at de-virtualizing the dispatch, removing the hefty performance pentalty.

This is why even basic arithmetic operations like +, * and whatnot are able to be generic functions (with a gigantic number of methods, 184 for + and 364 for * currently just in Base and LinearAlgebra!) without any runtime performance compromise.

By contrast, Nim for instance just removed multiple dispatch in their 1.0 release because nobody was using it due to being so slow.

Thanks! I always suspected you could have a fast dynamic dispatch in a typed language if you locked it down enough to not allow redefinitions at runtime and such (which removes a lot of the fun of CLOS). It makes sense to let the jit do the heavy lifting though!
Yes, Julia definitely had to make some dynamism restrictions that I suspect would dismay some lispers, but it is still a quite dynamic language. The language devs do some pretty clever things to have our cake and eat it too.

On the flip side, I think we have some dynamic metaprogramming magic of our own that might even impress some lispers such as IRTools.jl's dynamos or Cassette.jl's overdubing.

Multiple dispatch is a wonderful thing. It's basically what OOP should have been, rather than the horrible cludge of classes.
Golang's level of boringness. It's just boring enough to still be useful. Often it's easy to understand random code and write "reasonable" code.
Nullability built into the type system like Kotlin. one main difference between kotlin optionals and swift options (for example) is that Kotlin nullability is enforced during compile time.

I'm not aware of many languages which do that other than Kotlin.

(comment deleted)
Every language should have robust, standard collections functions like Python does. Once you start doing quick things with lists like the zip function, or list comprehensions, it's hard to go back.
> Once you start doing quick things with lists like the zip function, or list comprehensions, it's hard to go back.

Totally agree with you on this one, but I'm not sure I'd go so far as to say that they're a defining feature of Python, more of functional languages, from which Python has borrowed a thing or two.

Agreed! Python is just one example of a language that does it well.
Personally I think Ruby does a much nicer job of this, although I use both languages on a regular basis. A complx chain of map and filter calls tends to be a lot more readable than a complex nested list comprehension.
Python collections doesn't have basic data structures like red black trees (which are in java as java.util.TreeMap or c++ as std::map). You end up needing third party libraries like sortedcontainers.
algebraic data types and parametric polymorphism, so you can define an option type for computations that fails and get rid of nulls
Factor allows almost any special character in word (function) names. It is incredibly useful when you get the hang of it. I would love to have it in other languages.

E.g. is_prime(1234) => prime?(1234), append_inplace(l, e) => append!(l, e), array_equal(a, b) => array=(a, b), midi_to_mp3(a) => midi>mp3(a), reciprocal(a) => ^-1(a)

Lisp can do some of it but not "()" or (I think) "`';"

In Common Lisp you can use pretty much any character in a symbol name, if you escape it:

  [3]> '|(``"|
  |(``"|
  [4]> (defvar |(``"| 42)
  |(``"|
  [5]> |(``"|
  42
It's pretty dubious for code; it's useful when working with some data format that has identifiers with its own validity rules.

There is more than one way to escape: vertical bars or backslashes:

  [6]> (eq '|(``"| '\(\`\`\")
  T
Kotlin Extension Functions

https://kotlinlang.org/docs/reference/extensions.html

I personally love it for when I have different data classes for different layers of the application (REST objects vs. controller level objects vs. Database objects) and need to convert between them. I simply just add an extension to one layer's data object to convert it to the other layer's type