Live data from Hacker News

Why static languages suffer from complexity

hirrolot.github.io

281–290 of 306 posts

Re: Why static languages suffer from complexity

#281
post #180

Earlier quoted context omitted.

Its use is kind of a code smell, and I believe it was a relatively (prolog is old) late addition. In any case, I wrote "doesn't need", though perhaps you consider that hair splitting.

I think you misinterpreted that. Prolog does has an if operator -> as in ( P -> If ; Else ) yes, but oldsecondhand said :- "predicateA is true" if "clauseB is true" and "clauseC is true". or in prolog predicateA :- clauseB, clauseC. so :- is if, just in it's own, Prolog-y way.

Is that an "if", though? In C/C++, you could write that as:

  if (clauseB && clauseC)
    predicateA = true;
  else
    predicateA = false;
which is clearly an "if". Or you could write it as:

  predicateA = claseB && clauseC;
which is not an "if" at all, but just a boolean calculation. (Unless you regard all boolean calculations as "if"s in disguise...)

The prolog version seems to me to be more in the spirit of the second C version.

Re: Why static languages suffer from complexity

#282

Earlier quoted context omitted.

Why do you think that’s not a type system? Literally all type systems could be described as “a static analyzer” that tries to assign and validate properties over the code it’s analyzing. All compilers also rely on the results of that static analysis to direct codegen. Rust’s type system implements substructural typing, and the borrow checker is an integral element of that type system.

Based on your definition, then any static analyzer is a type system, because type information and the usage of those types is basically all that’s available for static analysis. Types define what operations can be performed. The borrow checker looks at allocations and determines when they can be freed. It really has nothing to do with types. An ideal implementation of the borrow checker could be totally type unaware…

> It really has nothing to do with types.

I was under the impression that it's substantially an implementation of an affine type system (https://en.m.wikipedia.org/wiki/Substructural_type_system).

Probably you could build such a thing without thinking about types per-se, but I don't think the designers of Rust did that, and I am not sure that makes it "not a type system" anyway.

> any static analyzer is a type system

"Any static analysis checking correctness is either a linter or a type checker" isn't a claim I would make, but I am not sure offhand how I would argue against it.

Re: Why static languages suffer from complexity

#283

Earlier quoted context omitted.

I think you misinterpreted that. Prolog does has an if operator -> as in ( P -> If ; Else ) yes, but oldsecondhand said :- "predicateA is true" if "clauseB is true" and "clauseC is true". or in prolog predicateA :- clauseB, clauseC. so :- is if, just in it's own, Prolog-y way.

Is that an "if", though? In C/C++, you could write that as: if (clauseB && clauseC) predicateA = true; else predicateA = false; which is clearly an "if". Or you could write it as: predicateA = claseB && clauseC; which is not an "if" at all, but just a boolean calculation. (Unless you regard all boolean calculations as "if"s in disguise...) The prolog version seems to me to be more in the spirit of the second C versio…

Well if nothing has any arguments then yeah, it's basically the second version. but if you had arguments you end up with things like

  %foo(+A,-B)

  foo(A,42) :-
     A = 10.
  foo(_,100).
which would be

  bool foo(int *A, int *B){
     if (*A = 10){
         *B = 24;
     }else{
         *B = 100;
     }

     return true;
  }  /* my pointer knowledge is rusty though, so grain of salt */

Re: Why static languages suffer from complexity

#284
post #280
post #252

Earlier quoted context omitted.

> not slowing one's work Put back into context, your reply makes sense as these popular libraries are pretty battle tested. Having said that, it is a valid point that type hints being voluntary means they can only be relied upon with discipled developers and for code you control. Of course, the same point could be made for any code you can't control, especially if the library is written in a weakly typed language lik…

So why can't Nim infer from let b: uint = a that you're really just saying let b: uint = uint(a) And BTW don't you get tired of typing (and reading) `uint` twice in the latter setting? That's what I mean about "side effects" after all.

> So why can't Nim infer from `let b: uint = a`

It "can", but it's a design decision not to by default because mixing `uint` and `int` is usually a bad idea.

This is telling the compiler you want to add an `int` that represents (say) 63 bits of data with a +/- sign bit to a `uint` that doesn't have a sign bit. If `a = -1` then `b = uint(a)` leaves `b == 18446744073709551615`. Is that expected? Is it a bad idea? Yes. So, the explicit casting is "getting in your way" deliberately so you don't make these mistakes. If `a` is a `uint`, it can't be set to `-1`, and adding them is freely allowed.

Incidentally `uint` shouldn't be used for other reasons too, for instance unsigned integers wrap around on overflow, whereas integers raise overflow errors. The freedom of mixing types like this are why languages like C have so many footguns.

In short, explicit is better than implicit when data semantics are different. When the semantics are the same, like with two `int` values, there's no need to do this extra step.

You could create a converter to automatically convert between these types, but you should know what you're doing; the compiler is trying to save you from surprises. For `int`/`float`, there is the lenientops module: https://nim-lang.org/docs/lenientops.html. This has to be deliberately imported so you're making a conscious choice to allow mixing these types.

> don't you get tired of typing (and reading) `uint` twice in the latter setting?

Well, no because I wouldn't be writing this code. This example is purely to show how the typing system lets you write pythonesque code with inferred typing for sensible things, and ensures you're explicit for less sensible things.

For just `int`, there's no need to coerce types:

    var
      a = 1
      b = a + 2
      intro = "My name is "
      name = "Foo"
      greeting = ""

    b *= 10

    # Error: type mismatch: can't concatenate a string with the `b` int.
    # greeting = intro & name & " and I am " & b & " years old"

    # The `$` operator converts the `b` int to a string.
    greeting = intro & name & " and I am " & $b & " years old"

    # If we wanted, we could allow this with a proc:
    proc `&`(s: string, b: int): string = s & $b

    # Now this works.
    greeting = intro & name & " and I am " & b & " years old"

    echo greeting # "My name is Foo and I am 30 years old"

    # Normally, however, we'd probably be using the built in strformat.
    # Incidentally, this is similar to the printf macro mentioned in the article.

    import strformat
    echo &"My name is {name} and I am {b} years old"

Re: Why static languages suffer from complexity

#285
post #273

Earlier quoted context omitted.

> Fascination with type systems does not seem to be all that useful in practice. > ... > The Rust borrow checker is an invariant enforcer. [...] This is real progress in programming language design, and is Rust's main contribution. I'm so confused by your stance here. You essentially say "type systems are not useful" and then "oh but this most recent advance in type systems — that one is useful." Do you find type sys…

It is probably pretty presumptuous to assume, but I think that a lot of programmers that have only every been exposed to C/C++/C#, Java and Python have basically no concept of what a good type system can do for them. Two examples from the top of my head: 1. Encoding matrix sizes into the data- and function-types, so that you can safely have a function `mat[c,b] mat_mult(mat[a,b] a, mat[c,d] b)` or even `mat[w-2,h-2]…

For example one, it works for signal processing or graphics but compile-time dimensions are unusable in Machine Learning or Numerical Computing because it's too much friction on serialization/deserialization and some operations that reduce dimension or rank are based on runtime data (for example some dimensions are 1)

Re: Why static languages suffer from complexity

#286
post #16

Earlier quoted context omitted.

Shader languages are also hellbent on avoiding branches too so if is frowned upon and often not used. I could easily imagine not having it in a shader language.

The old assembly-like languages (ARB_fragment_program, NV_fragment_program*, et al. ) did indeed not have branches, only selection and conditional termination, because that was the extent of the capabilities of the underlying hardware. (I understand the execution on modern fragment processors can’t actually diverge within a single batch, either, so they execute both branches and select afterwards, but they are at lea…

Still true. Cuda warps work by team of 32 threads and if there is a branch they have to take both and then select the result. It's fine for loop termination ``while (i < 1000)`` but if there is actual work it's often significantly better to switch to branchless code.

Re: Why static languages suffer from complexity

#287
post #149

The problem I find with static typing is that it so easily leads you over-specifying the requirements / constraints. In fact, it makes such a virtue out of that over-specification that many people would consider it a best practice to do so. For example, perhaps my `calculate_price` function only depends on 2 attributes of the order which has 65 attributes. Am I creating a 2-element data type for that function to proc…

In addition to what others have said about just passing two parameters, there also row types, where the signature of `calculate_price` can be specified to accept any record that has the two required fields.

Isn't that duck type?

Re: Why static languages suffer from complexity

#288

Earlier quoted context omitted.

>your programs evolve very rapidly. Since with dependent types you loose type-inference, you now what to evolve two programs rather than one. Yes, just like you have to evolve your specification/documentation. Similarly, in the exploratory phase you'll stick to very 'rough' typing and next to no proofs and as the program gets clearer and solidifies, you can continuously refine your types (with the amount of refinemen…

> just like you have to evolve your specification/documentation. That is correct, and also one of the core reasons why in the vast majority of cases either no specification/documentation exists, or will only cover a small case of the actual specification. For example I would bet money that not a single function in the C, C++, Java and Python standard libraries is fully specified, in the sense of nailing down the prog…

>the core reasons why in the vast majority of cases either no specification/documentation exists

I feel that is much too pessimistic.

>will only cover a small case of the actual specification.

If the same applies to proofs: so be it. Don't let perfect be the enemy of good!

>For example I would bet money that not a single function in the C, C++, Java and Python standard libraries is fully specified, in the sense of nailing down the program up to observational equivalence.

I'd imagine so as well, but I think that's more indicative of how even a (superficially) simple language like C is not all that amenable to specification.

> A lot of code refactoring I've done was trivial (e.g. changing the order or arguments), but ripples through the program and proof structure.

This is not my experience. If you use sufficient proof automation, something like this should have next to no impact on proof structure. Univalence is useful, but a lot of refactoring is not just switching to isomorphic representations, so I'm convinced that larger scale proof automation is way more essential than HoTT.

> Java exception specifications

I'm not convinced that this is fundamental to specifying exceptions rather than just Java having particularly poor ergonomics for it. I've never met a person that actually liked implicit exceptions and if you ask people who dislike exceptions, that's often one key part of it.

> In contrast, expressive type-theories constantly force you to prove a lot of trivialities.

For all large dependently typed languages that actually bill themselves as programming languages (Agda, Idris, Lean) there is some sort of 'unsafe' feature that allows you to turn the termination checker off - at your own peril of course. But you only pay for what you use, if you don't need the additional correctness, you don't need to put in any more work, just like with unit tests.

(There are also ways to safely defer termination proofs, but to be fair the experience isn't the best currently.)

>I don't know what you mean by declarative (other than: leaving out some detail).

Specs tell you the what but not (necessarily) the how. Take the spec for all sorting algorithms (giving observational equivalence):

1. The output list is a permutation of the input

2. The output is sorted in regards to some predicate.

That's a couple lines at most (or a one-liner if you don't count the building blocks like the definition of a permutation), which is a good amount shorter and easier to verify than e.g. a full heapsort implementation.

>But they cannot be smaller in general: if every program P had a full specification S that was shorter [...] then you've an impossibly strong compressor

The compression is stripping away the 'how', that's why you can't 'write a spec for a spec' and compress further.

>What you see in practise is that you only specify some properties of the program you are working on.

Sure, writing a fully specified program of non-trivial size is currently really only possible if you're willing to ~waste~ invest a decade or more.

>Moreover, if you only work with a partial specification, you can ask the question: what level of partiality in my specification gives me the best software engineering results.

Why would you assume that there is a single level of partiality that gives the best results? I agree that HM style types are a great fit for 'general' programming because it has such low impedance, however I also believe that most programs have some areas where they would benefit from more specification. (I think people have a clear desire for this and that it's at least partially responsible for some of the crazy type hackery as seen in Haskell or Scala, which could often be greatly simplified with a richer type system.)

Having a richer type system doesn't mean that you always have to fully use it. It's perfectly possible to just use a HM style/System F fragment. Using dependent types just for refinement is already one of the dominant styles for verification. If dependent types ever become mainstream in any way, I imagine it will also be largely in that style.

Re: Why static languages suffer from complexity

#289

Earlier quoted context omitted.

It's not a fascination, it's just easier and better to have good static analysis when programming. That doesn't have to be a type system, but I think there is a lot of reason to think that a type system is the lowest hanging fruit for useful static analyses.

I think this sums up the pragmatics well. Brian Cantrell discusses in one of his talks what they did at Sun to ensure they were writing safe C. This was a substantial amount of tooling they had to build up. Type systems bring you this tooling in a well founded, logical way. And as you say, it's a good place to start, even if it's just to know how the puzzle pieces of your code fit together.

Yes exactly. I'm kind of a broken record on this, but the key thing is static analysis. It's just that with statically typed languages, the type system specification and its implementation give you a giant head start on doing those analyses. You can build other kinds of static analyses for languages without static types, but it's just harder and you're way more on your own; you don't benefit from all the work put into the compiler for the language.

Re: Why static languages suffer from complexity

#290
post #280
post #252

Earlier quoted context omitted.

> not slowing one's work Put back into context, your reply makes sense as these popular libraries are pretty battle tested. Having said that, it is a valid point that type hints being voluntary means they can only be relied upon with discipled developers and for code you control. Of course, the same point could be made for any code you can't control, especially if the library is written in a weakly typed language lik…

So why can't Nim infer from let b: uint = a that you're really just saying let b: uint = uint(a) And BTW don't you get tired of typing (and reading) `uint` twice in the latter setting? That's what I mean about "side effects" after all.

Oh, just to add that

    let b: uint = uint(a)

    # can be written as:
    let b = uint(a)
The type is inferred from the right hand side during assignment. The only reason I wrote this

    let b: uint = a
is because in my example `a` was an `int`, so

    let b = a
Would infer an `int` type for `b`, which compiles fine, and doesn't show the type mismatch I wanted to present.
Post reply on HN