Hacker News new | past | comments | ask | show | jobs | submit login
Not Explicit (boats.gitlab.io)
147 points by kawera on Dec 30, 2017 | hide | past | favorite | 32 comments



I didn't agree with this article's definition of "explicit," which feels tautological:

Rust is explicit because you can figure out a lot about your program from the source of it.

But as long as the source code conveys the information, it is still explicit in the narrow sense I defined above.

But if the thing will happen deterministically in a manner that can be derived from the source, it is still explicit in the narrow sense that I laid out earlier.

Source code conveys everything possible about your program in any language.

The best notion of "explicit" is captured by the critique of "local." Here's some nice ways in which Rust is more "local" than C++:

1. Macros require bang!s instead of C++ surprise-insides

2. References require &s and `mut &`s instead of C++ implicit references.

3. Int -> FP conversions require a typecast

In every case, both Rust and C++ allow you to discover the runtime behavior by reading the source code. But I would argue that Rust is more explicit than C++ in every case, wouldn't you agree?

A feature of Rust that is not implicit, but also not local, is method resolution

Isn't method resolution intimately tied to Deref coercion, which allows a type to implicitly (Rust's language) implement methods of another type? So method resolution seems like one of the more implicit features within Rust.

but all of it is explicit if you look at the impl blocks for Vec<T>.

Well, all of Ruby's monkey-patching is explicit in the sense that you can find it if you know where to look.

It's a good effort to refine the meaning but "local" IMO is the best one!


Your example comparing to C++ is something where I think the point of this article is coming out: we can become more explicit about what we're talking about when we bring nuance into our language to describe things like 'local' vs 'statically determined' which in language design discussions are both being labelled 'explicit' despite being distinct concepts. Explicit has been overused to the point of being a bad word


As in "explicit content"?


>> but all of it is explicit if you look at the impl blocks for Vec<T>.

> Well, all of Ruby's monkey-patching is explicit in the sense that you can find it if you know where to look.

There's a big difference. All impls in the first case can be found in a specific module and are listed in the generated documentation. If you want that information, there's one place to look.

In case of any monkey patching, that's not the case. It can happen literally anywhere, it can happen conditionally, and the code doesn't even have to mention the patched class since it can receive it as a parameter, which makes it impossible to even grep for.


So the definition of explicit or not has to do with the computability of the question.

Method resolution is not computable in a dynamic language with monkey patching. Hence, not explicit.

That's a good point.


Another way to think about it is whether the IDE can do it for you. As a Java dev (who is admittedly a bit of a slave to IntelliJ) this is a huge part of my programming experience.


A heavyweight IDE is really only necessary because of the unwieldiness of Java patterns for naming and code layout. Autocompletion and navigation leads to code that is only manageable with those features in a vicious cycle.


> Source code conveys everything possible about your program in any language.

I disagree with this statement. Behaviour of the program will be determined by machine instructions that the compiler generates for a given source plus the machine environment where these instructions will be run. And it is not guaranteed that the generated instructions will be same for a given source across different architectures and environments that the compiler is run (plus I guess there is generally some degree of non-determinism in the compilers as well, because of the different optimizations it can make under different conditions, which are not "explicit" from the source itself).

As far I understood, in the notion of explicitness that is defined in the article, it is defined that the language where source almost completely specifies the behaviour of program, irrespective of the compiler environments where it is compiled on, is an explicit language.

I think in the sibling comment https://news.ycombinator.com/item?id=16034541 bad_user articulates more clearly the point I was trying to make.


The behaviour is usually determined by language specification, and there are many machine instructions that can cause the correct behaviour. There are some cases where languages have machine-dependent behaviour, but most parts of most languages define the semantics independent of the machine it's running on.

> As far I understood, in the notion of explicitness that is defined in the article, it is defined that the language where source almost completely specifies the behaviour of program, irrespective of the compiler environments where it is compiled on, is an explicit language.

I've never seen anyone use this definition for "explicit". Most people use "explicit" to describe syntactical differences (e.g. "explicit vs implicit self/this"). "Implicits" in Scala is where the compiler inserts a method/function-call that didn't exist syntactically. The behaviour is still well-defined and not dependent on the machine instructions.

What you're describing sounds to me more about the language defining its own execution model that's abstracted above actual computers.


What bothers me about languages like Rust, which I think I'm in the minority on, is the lack of explicit type declarations.

C and Java, my two first languages, strictly spell out what types they expect. Sure you can ignore those rules but as long as you don't those types convey a lot of useful information about your program. With languages like Rust I don't think I could ever start writing code in them without an IDE. The entire idea of compiler-specified types is neat but I don't think all of your code should rely on that.

One oddity is Python which I have used a lot. The only difference is that Python is much less strict than the languages I'm used to. Because of this learning Python was still relatively difficult (Why is all the documentation written like a book!? I miss my Doxygen) but I didn't need an IDE.


> is the lack of explicit type declarations

This is mistaken. Rust has explicit type declarations, augmented with type inference. Furthermore, unlike popular statically-typed functional languages, Rust's type inference is deliberately restricted to only operate within functions (intraprocedural) rather than between functions (interprocedural), which means that one always has explicit types available in the function signature when reading code.


Perhaps not type declarations, but I think GP is onto something when it comes to Rust and how it handles types in a way that often lacks explicitness. There's a lot of Rust code where you have to have familiarity with an API to understand what's going on with the types.

As a for instance, let's say you have a function:

    fn connect(addr: std::net::IpAddr) -> ... {
        ...
    }
And it gets called:

    connect("192.168.0.1".parse());
Unless you have a pretty good level of familiarity with the APIs involved, it's pretty difficult to reason about the types when looking at just the invocation of connect. You'd have to know that str::parse() will figure out what type it's supposed to be and, if it implements FromStr, it will call the from_str() function defined for that type to handle the coercion. That's a frustrating level of indirection for a beginner. And, since all of this is defined in the std library, it's also possible for library authors to similarly declare APIs that have this kind of indirect invocation where you only really understand what's going on if you've studied/memorized that API. I've often thought that a maze of From/Into and Deref coercions would be an excellent approach to the underhanded Rust competition because it's often so difficult to realize when that code is getting executed and it would be pretty easy to hide malicious code in functions that are executed implicitly.

It also doesn't help that a sizable portion of the Rust community believes console-based editors like vi and emacs are preferred and more advanced Rust IDEs are still early days and aren't able to help out much with situations like these.

It's not that I disagree with these types of language design decisions. I just feel this kind of lack of explicitness makes the language optimized for experienced users rather than beginners and helps give Rust it's deserved reputation for having a high learning curve.


Dude, just try it. I see where you're coming from, but in practice I find that the reduction in obviousness to be small, and the benefits of reduced typing and noise in my program to be huge. I've come to see type declarations as work fit mostly for computers and I'm almost personally offended at languages that make me do it for them. So basically, if you try type inference, I don't think you'll want to go back. :)

P.S. I hear the Rust plugin for IntelliJ is pretty solid.


Interestingly, even Java is probably going to introduce local variable type inference using syntax somewhat like

    let foo = bar();
http://openjdk.java.net/jeps/286


I'm torn on this. I prefer explicit types in declarations as well, but very much like the convenience of inferred types. I find myself thinking, though, that the demand for the convenience of inferred types in these languages is a result of the verbosity in the type specifications themselves, usually with generics (Rust) or templates (C++). I rarely use the convenience of inferred types for concise type labels (e.g. non-generic structs, integers, etc.). Maybe this is a deficiency in the languages' designs?


We put a lot of consideration into this. Basically, we could let you elide types everywhere, but didn't. There's a few reasons why, and they're sorta inter-related.

If you let type signatures be inferred, then you can get very poor errors. This happens when you change some code in the body, that changes the type signature of the function, which then causes an error to happen where the function is used. This misdirects from the source of the error, rather than helping you figure out what goes wrong.

Conceptually, signatures are the place where contracts are enforced, especially in Rust. Everything about a function should be expressed in its signature, so that way we can do analysis based on only the signatures. This helps in a few different ways: it's faster for the compiler, and it's also easier for the human reading it.

The bodies of functions, however, aren't exposed to the greater world. With a defined signature, the compiler can give you great error messages, pointing exactly to where you made a mistake. In this case, inference makes sense.

It's not just about convenience though. You can see this in languages like C++, which optionally support type deduction (but not full inference):

  auto a = 1 + 2;
  int a = 1 + 2;
With auto, the type is deduced. Without auto, you specify the type.

There are various arguments that range between "never auto unless you have to" and "always auto".

Consider this example https://stackoverflow.com/questions/6900459/the-new-keyword-...

  SomeType<OtherType>::SomeOtherType * pObject = new SomeType<OtherType>::SomeOtherType();

  auto pObject = new SomeType<OtherType>::SomeOtherType();
Here, auto improves readability, because you end up just repeating the type over again. This was my main frustration with Java's generics back in the day. (I primarily used Java in the 1.4 ~ 1.5 era, I know today's is a very different beast!)

This is directly related to the parent post; some might say "explicit is better than implicit" but in my opinion, while that's often true, implicit is better than explicit sometimes. The above is a great example. The post is all about trying to tease out better words to talk about these differences.


I think there is a lot of useful information, within a function body, that can be created by inclusion of specific types. I don't think your decision was bad. People obviously like the language and the compiler's type inference systems are apparently extremely advanced.

In my opinion this is an anti-pattern for beginning developers and complicates the development of IDEs. This is for a number of reasons.

    1. Saving screen space is not a good reason to omit types
Long long ago the people typing away at Bell Labs made these same decisions. All across the UNIX kernel and user space they used partial/shortened names. This did not help users nor did it help developers. K.T. is said to have joked that the missing "e" from "ceat" is his biggest regret from the UNIX system.

We don't use 80x40 terminal displays and most of us type at faster than 25wpm. The space or time saved when writing...

    let people = new List<String>();
When compared to...

    List<String> people = new List<String>();
Is not worth it in my opinion. This is even more so when you have situations like this

    List<String> people = getGuestList();
vs

    let people = getGuestList();
In my opinion this is considerably more difficult to understand what type I'm working with. I have to now go and read the header for `getGuestList()` to find out what I can do with my `people` variable.

    2. IDE development will be more difficult.
This is minor, and will eventually be a solved problem, but writing an IDE that doesn't have immediate type information for everything in scope will be more complicated. Some may even say this might be impossible to accomplish via static analysis. Because of this the development of things like the RLS are much more difficult.

    3. The seasoned but new developer will struggle to find how things work.
When I pick up a new programming language at this stage in my career I no longer read through a book or documentation. I sit down and I try and accomplish a project. I find a task, find some code that does the subsets of the problems I want to solve, read that code, then implement my own solutions with the knowledge that I've gained. With Rust this approach doesn't work (for me). I have not been able to pick up Rust. I don't know what kind of objects are being used to hold which kind of data without reading almost every single line of code in the program (or at least everything accessible from the scope I am interested in).

In Java, C, etc I get some clue as to what's going on. In Java I need to only read maybe the first few words on every line to get an idea of what will happen or what is being used.

    public boolean isOnGuestList(String person) {
        List<String> people = ...;
        List<String> plusOnes = ...;
        return people.contains(person) || plusOnes.contains(person);
    }
I might not know what a List<String> is but I can easily find out.

This biggest effect this has is that since I have been programming for a long time I have an intuition where certain language features will be used. Every* language has a KV store, a list, some way to interact with file descriptors, etc. I know what kinds of programs will do those things and I can look at those programs for reference. Unfortunately for languages like Rust I need to already know the general types that people use to understand the code. All I would see is...

    fn isOnGuestList(person: string) -> string {
        let people = ...;
        let plusOnes = ...;
        people.contains(person) || plusOnes.contains(person);
    }


I hear you. A lot of this is based on opinion; you find the duplication good, I find it near-unbearable. :)

One little objective thing though:

> but writing an IDE that doesn't have immediate type information for everything in scope will be more complicated. Some may even say this might be impossible to accomplish via static analysis. Because of this the development of things like the RLS are much more difficult.

The compiler is static analysis. If the compiler can figure it out, then so can any other tool. And in fact, the RLS works by asking the compiler directly, so it's not actually any harder. Well, there has been some refactoring needed within the compiler to support this, but it's the same interface as incremental re-compilation (which needs to ask the same questions) so it was gonna happen anyway.

I do think that IDEs are a way to split the difference here; you can hover over people or plusOnes and see the type.


> I hear you. A lot of this is based on opinion; you find the duplication good, I find it near-unbearable. :)

I don't really find it good. I just don't see any easier to follow alternatives. Imagine getting a contract to add a new feature to C code written for an old mainframe in the 80s. It's a pain, right? Now imagine you just deleted half of the type annotations and replacing them with let. Do you think that would be easier to read? Maybe a better middle ground is left-justified definitions of types? I don't know.

I think that

    vec<int> something = new;
might be better than

    let something = new vec<int>;
We read left to right, the first thing on the line is the left most item, and the left side of the variable definition with always be visible (can't have a function call on the left hand side).

> If the compiler can figure it out, then so can any other tool

I understand that but most IDEs, outside of the ones supporting the new language server initiative, don't really talk to the runtime/compiler. It's also the case that architecturing your compiler/IDE this way is very computationally expensive. I'm on an x220 which is only a few years old and RLS locks up every time I try to load a non-trivial project.

> I do think that IDEs are a way to split the difference here; you can hover over people or plusOnes and see the type.

It would help but I have not had any luck getting a decent IDE setup for Rust. I know this is something that I am not alone on and (thankfully) this is a criticism the Rust community is very receptive to. I'd be really interested in hearing what you and many of the other core devs do for your environments.


> vec<int> something = new;

> might be better than

> let something = new vec<int>;

I don't think this is right at all. The name of the variable is far more important than its type - ideally, the name of the variable should include a hint as to the nature of its type, whether it's explicit (e.g. parent and child in a tree navigation function are obviously references to the tree node type that is being navigated over) or very lightly suggested (e.g. pluralization, like children for a list of child things). The actual type is of secondary importance, chosen for implementation reasons (e.g. a map might be hash-based or tree-based, or a list might be a view over some other list, synthesized from some foreign collection, or a wrapper around an array in memory).

For me, the name of the variable is of paramount importance for understanding the code. The type is very secondary. Types are usually in two buckets: domain-specific (so the name typically is highly relevant to what's being done and is often the same as the type name or an abbreviation or agglutination), or common denominator library types (typically collections, strings, primitives, that kind of thing) where the name of the variable implies the type.

The name is the thing that you see over and over again in the code. You also don't get to see the type when you call a method. So readable code needs good consistent names that make type annotations helpful but far from mandatory for comprehension.


I use VS: Code with the RLS and the plugin we provide for it, that's it. I also sometimes use vim with syntax highlighting, but no RLS or anything fancy.

> RLS locks up every time I try to load a non-trivial project.

I wonder if this is the initial build; that is, right now, RLS basically needs to build your code in order to do its thing, so the first time you open stuff, it's gonna be slow and make your fans whir. After that it shouldn't be a huge deal.

Things have been a bit wonky as of late, but the RLS is now riding the trains at least, so being available on stable will be very nice.


> We don't use 80x40 terminal displays and most of us type at faster than 25wpm.

But reading and understanding code is still the most time-consuming part of programming, and when a function or class stops fitting on a single screen that's a huge hit to readability, whether that's 40 lines or 100.

> have to now go and read the header for `getGuestList()` to find out what I can do with my `people` variable.

No you don't, you just mouseover it or press a simple key shortcut and get it.

> This is minor, and will eventually be a solved problem, but writing an IDE that doesn't have immediate type information for everything in scope will be more complicated. Some may even say this might be impossible to accomplish via static analysis.

Huh? The IDE already has to do all the work anyway because it already has to compile and check the code. It's no harder for the IDE to use the inferred types, assuming the compiler was suitably modular.

> When I pick up a new programming language at this stage in my career I no longer read through a book or documentation. I sit down and I try and accomplish a project. I find a task, find some code that does the subsets of the problems I want to solve, read that code, then implement my own solutions with the knowledge that I've gained. With Rust this approach doesn't work (for me). I have not been able to pick up Rust. I don't know what kind of objects are being used to hold which kind of data without reading almost every single line of code in the program (or at least everything accessible from the scope I am interested in).

Switching from an Algol-family language to an ML-family language is a bigger change than switching between two Algol-family languages. It's going to be more work. But improvements are always changes.

> Unfortunately for languages like Rust I need to already know the general types that people use to understand the code. All I would see is...

Why's that a problem? Particularly since you said you already know Python, isn't that a perfectly sensible bit of code to read? Write what you would in Python, but get faster feedback on whether it's right (compile failure rather than runtime error) - isn't that something worthwhile?


In Rust, you still have to declare types for function parameters and return values. So, if you prefer to declare types everywhere, break your code down into single-expression functions!


And even then, you are still allowed to explicitly define types everywhere, they are just optional.

https://play.rust-lang.org/?gist=24d87fde3cc5273d4ec73ba5647...


I have bad news for you, local type inference is coming to Java :)


Not sure what he was going for with that definition of "explicit." It wasn't explicit at all.

> you can figure out a lot about your program from the source of it.

How does "implicit" not satisfy this condition? I can tell a lot about a program from the source of it implicitly as well.


What a useful breakdown!

It seems like the narrow definition of “explicit” that the author uses is equivalent to “deterministic”. If that’s true, then “explicit” could always be replaced with a more precise alternative.


No, I don’t think that’s what the author is saying, but to make it clear the definition is:

“In computer science, a deterministic algorithm is an algorithm which, given a particular input, will always produce the same output, with the underlying machine always passing through the same sequence of states”

In turn what the author is saying is precisely what he says, no more, no less - explicit code is code whose behavior you can understand just by reading it, with no external context, like how the runtime currently works on a particular hardware.

The examples he gives are very clear as well. For example he claims to know the stack allocations that will happen just by looking at the definitions of the data types. You can’t know this on the JVM for example because the JVM does optimizations under the hood that the programmer cannot control or reason about. This doesn’t yield non-determinism. What happens is that the developer ends up programming for a higher level machine that hides some details of the underlying hardware.

Explicit isn’t always better than implicit, an argument that the author also makes.

Overall I find the article very compelling and I’ll be sure to use it for reference, as it’s what I also think, but with better words.


> In turn what the author is saying is precisely what he says, no more, no less - explicit code is code whose behavior you can understand just by reading it, with no external context, like how the runtime currently works on a particular hardware.

I'm not sure where this definition is coming from. Let's look at this code:

    vec.len()
I understand this behaviour very clearly. This will invoke the method given the following rules:

- Look for `len` on the type of `vec`

- If `vec` implements Deref it will look for `len` there

- Look for `len` on traits that the type of `vec` implements

- And so on…

Yes, the behaviour is conditional, but it's still well-defined and easy to understand, although complex.

"Implicit" is usually used when you want to describe situations where there are multiple interpretations, and one is chosen due to context. In Ruby:

    first_name + " " + last_name
In this example `first_name` can either be a local variable or a method call (which can either be in the same class, an included module, or a method_missing). Here it turns out it was a method call, and we therefore call this an "implicit method call". I can rewrite:

    self.first_name + " " + self.last_name
In this case there is a single interpretation: It can only be a method call.

However, while this code is explicit on whether or not it's a method call, it's not explicit about which method is being invoked. Usually this implicitness is considered a good feature since it allows abstraction, polymorphism and so on.


Not really, programming languages behaviours are almost always deterministic (UB + compiler shenanigans aside). For instance the weirder corners of JS value coercions are exhaustively documented and deterministically followed by browsers, but I would argue they're not explicit, unless you're bi-classed language lawyer or have already been bitten several times, you don't "see" this (again completely deterministic) process when reading the usually innocent-looking code which ends up invoking it.


It is worrying that you have to explicitly/verbosely write article about explicitness, I thought people knew.


To the contrary, the article about _implicitness_ is the one that doesn't need to be written because people already know what it means.




Join us for AI Startup School this June 16-17 in San Francisco!

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

Search: