HNHacker News
TopNewBestAskShowJobs

catnaroek

1,638 karma · joined August 18, 2013

The end of the world is nigh. Bring as much popcorn as you can!
submissionscomments
catnaroek··on When Functional Programming Isn't Functional
I can't prove anything about programs other people will write, unless the language itself makes guarantees about the programs they can write. Culture doesn't help one iota.
catnaroek··on When Functional Programming Isn't Functional
> Even if their encoding has identify, immutable value don’t need it.

Values indeed don't have physical identities. The problem is that, when you use a language with pervasive object identities, the values you want only exist in your head, not in the semantics of the language you are using.

> Languages like C# allow you to throw off identity altogether with pass/store by value structs.

Please teach me how to define list or tree values in C#, sensei.

catnaroek··on When Functional Programming Isn't Functional
> Immutable objects do not have identity and are values.

Really? https://ideone.com/mmQJGr

> And the distinction between primitives and objects in Java, for our discussion here, is irrelevant.

All it takes to disabuse you of this notion is a little bit of reflection.

> That Haskell can force purity via its laziness

This is not true. Purity is enforced via the type system.

> but not required for doing FP, just like how static types aren't required for doing FP either

Completely agree here.

> but they've been preceded by LISP and ML.

ML is a functional language. Scheme already forces you to squint your eyes a lot. Lisp is not a functional language by any stretch of the term's meaning.

catnaroek··on When Functional Programming Isn't Functional
> Java's semantics permit defining pure functions, immutable values

Only by social convention. Not enforced by the language's semantics in any meaningful way. In fact, Java does not even allow the programmer to define custom values. All values are primitives or object identities. Note that so-called “value objects” will not fix anything.

A fundamental property of functions is so-called “function extensionality”. Two functions “f, g : Foo -> Bar” are equal if and only if “f(x) = g(x)” for every “x \in Foo”. In particular, this means that there should be no equality testing operator for functions, because function equality is undecidable.

---

OCaml at least has bona fide values. It allows you to implement procedures that evaluate interesting functions (i.e. value-to-value mappings), even if it doesn't typefully distinguish a special class of procedures that are syntactically guaranteed to evaluate functions.

F# and Clojure are lost cases, though.

As for Haskell's IO and IORef (or ML's ref), there is absolutely nothing wrong with the ability to implement procedures that do other things besides evaluating functions. The problem is when you lack the ability to express interesting functions, and the root cause is the lack of a sufficiently rich universe of values.

---

Your mention of purity completely misses the point. The ability to do traditional imperative programming is a feature, not a bug! The problem here is the inability to do functional programming. The reasons for this are technical, not cultural.

catnaroek··on When Functional Programming Isn't Functional
Sure, but that would be an object language embedded in the lambda calculus, not the lambda calculus itself.

btw, variants of the lambda calculus do exist where some values (so-called “reference cells”) are object identities. But a value cannot be a lambda abstraction and a reference cell at the same time.

catnaroek··on When Functional Programming Isn't Functional
Closures are simply an implementation detail of first-class procedures. But functional programming, unlike object-oriented programming, isn't just about making procedures first-class. It's about using, to the greatest extent possible, procedures that evaluate functions.
catnaroek··on When Functional Programming Isn't Functional
> Lisp was based on the lambda calculus, not sure if you can get more functional than that.

I've tried really hard (okay, for about five minutes), but couldn't find any account of the lambda calculus that includes the following features:

(0) variadic argument lists,

(1) object identities for absolutely everything,

(2) procedures that can be manipulated in ways other than applying them to arguments.

Are you really saying that all this time my understanding of the lambda calculus has been a lie?

catnaroek··on When Functional Programming Isn't Functional
> Functional Programming is programming with mathematical functions (aka pure functions).

Exactly. To which the following must be added: Functions are mappings from values to values. It makes no sense to try to do functional programming in a language whose semantics does not provide a rich enough supply of values.

> And an FP language is one that has features, but also a culture that encourage FP as the main paradigm for solving problems.

Culture matters very little. What really matters is what the language's semantics allows.

catnaroek··on Gradual Programming
Even if your dependencies are not formally verified, it is useful to formalize your expectations of them.
catnaroek··on Gradual Programming
Theorem: Whenever `numbers[index]` is used, `0 <= index < numbers.length`.

Proof: Use the loop invariant `0 <= index <= numbers.length`. The invariant holds before the loop begins because `index == 0` (established by you) and `numbers.length >= 0` (established by the implementor of the array type) imply it.

Inductively assume that prior iterations of the loop preserved the invariant and didn't perform any invalid indexing operations. Now, every time the loop's condition is evaluated, there are two possibilities: either `0 <= index == numbers.length` or `0 <= index < numbers.length`. In the former case, the loop terminates and no further indexing operations are used, establishing the claim. In the latter case, `numbers[index]` is a valid indexing operation, and the subsequent `index++` operation does not destroy the invariant, because, given the precondition `0 <= index < numbers.length`, the operation `index++` establishes the postcondition `0 < index <= numbers.length`, which implies the invariant.

---

Note, however, that in this case it would have been more efficient to use an iterator, because the iterator's implementation already packages this proof in a reusable way, so that you don't need to reprove it every time you write a loop similar to this.

catnaroek··on Gradual Programming
> I've never yet had the luxury of working in a language that admitted formal proofs of correctness.

You don't need much from your language, except for a formal semantics. But, even if your language of choice doesn't have one, you can stick to a subset of it that is easy to formalize: no first-class procedures (or things that can simulate them, such as dynamic method dispatch), no non-local control flow operators (e.g. raising and handling exceptions), no non-lexical variables, just statically dispatched procedures and structured control flow constructs (selection and repetition, i.e., pattern matching and recursion).

> When a bug occurs (perhaps because your tests did not anticipate every edge case) how do you determine where the problem code is located without a stack trace?

By reasoning about every statement's intended and actual preconditions and postconditions, i.e., predicates on the program's free variables. At some point, the intended and actual preconditions of a statement-postcondition pair won't agree. But this is unreasonably hard if you need to reconstruct these preconditions and postconditions completely from scratch.

catnaroek··on Gradual Programming
Yes. This is the only way you can justify that you know better than the compiler.
catnaroek··on Gradual Programming
What I need is the proof that the program is correct. Bugs will show up as gaps or inconsistencies in the proof. But, in the absence of even an attempt at a proof, all I can do is throw the program to the dogs.
catnaroek··on You can't Rust that
> in JavaScript, not all values are immutable.

Object states are mutable, of course. But they are not values. You can't bind object states to variables. If you use objects as proxies for values that exist in your head but not in in JavaScript's semantics, that's fine, so long as you keep in mind the distinction between what you wish you were using and what you are actually using.

catnaroek··on Gradual Programming
Stack traces contain a lot of information. Most of it is useless garbage. Yet at the same time sometimes stack traces don't contain the information you need. Making them even more useless garbage.
catnaroek··on Gradual Programming
This is a very good argument against mandatory automatic static checks, so long as they are replaced with equally mandatory manual proofs, included in the program's comments and/or documentation. But it does not justify dynamic typing in any way. For example, most programming languages offer no way to statically verify array indexing operations, so I just get my hands dirty and prove my array indices correct the old fashioned way (brain, paper and pen). But runtime bounds checking still annoys me, because they branch on a bit whose value I know beforehand, creating an unreachable control flow path.
catnaroek··on Gradual Programming
> This is to say, I don't think inference and gradual systems are necessarily at odds, but could potentially work together, although that doesn't happen today.

Type inference demands some rigidity in the type structure of a language. You must have the right amount of polymorphism: the STLC has too little, System F has too much. If you want both type operators and type synonyms, then operators can't be applied to partially applied synonyms [0], because higher-order unification is undecidable. If you want subtyping, you also have to do it in a certain way [1]. More generally, the type language must have a nice enough algebraic structure to make it feasible to solve systems of type (in)equations, because that's how inference works.

Your post contains a link to a paper [2] that contains the following passage:

> In this paper, we present a way to achieve efficient sound gradual typing. The core component of this approach is a nominal type system with run-time type information, which lets us verify assumptions about many large data structures with a single, quick check.

I have no way to confirm this claim at the moment, but it seems entirely reasonable. Checking structural types requires doing more work. But it is this very structure that makes type inference possible and useful.

So, yes, it seems that inference and gradual typing will be difficult to reconcile.

---

Footnotes:

[0] http://cstheory.stackexchange.com/questions/32260/ (I am the author of this stupid question.)

[1] http://news.ycombinator.com/item?id=13781467

[2] http://www.cs.cornell.edu/~ross/publications/nomalive/nomali...

catnaroek··on Gradual Programming
> where to hold state

Wherever it makes the most sense for the algorithms you want to implement.

> interactions between components

Again, driven by the needs of the algorithms, and, most importantly, their proofs of correctness.

catnaroek··on You can't Rust that
> But it's clearly not what people mean when they talk about pointers (or references) being mutable.

The semantics of a programming language is what it is, not what you want it to be, unless you explicitly choose a language that allows you to express exactly what you mean.

catnaroek··on Gradual Programming
Not every optionally typed system is gradually typed. The purpose of gradual typing is to allow typed and untyped code to interact

(0) without destroying the safety guarantees of typed code,

(1) correctly assigning blame, if a runtime type error happens, to the specific untyped part of the program that causes the error.

Common Lisp's optional types don't quite fit the bill.

catnaroek··on Gradual Programming
The argument for dynamic typing (whether one agrees with it or not, I actually don't) has nothing to do with explicit annotations. Rather, it is that there are paths in the design space that lead to correct solutions but pass through several inconsistent attempts (i.e., containing parts that contradicting each other and/or the problem statement). It is useful (again, according to proponents, not me) to consider these inconsistent attempts valid programs so that programmers can obtain example-based feedback about the consequences of their designs. Logic and abstract reasoning are not everyone's forte, after all.
catnaroek··on Gradual Programming
> I wish I could do this with larger code bases but unfortunately, from my own experience, planning time is exponentially related to the projected lines of code.

Hence more work has to be done on expressing solutions to big problems as the composition of solutions to smaller problems. Which is fundamentally an algorithms problem.

> Current abstraction methods are subpar: they require inherent complexity, are language specific, and are not easy to change after you've using them (unless you have adapters on every part of the code base which in it's own right becomes a mess).

Programming languages exist to implement algorithms, which in turn exist to solve problems. If your language of choice is getting in the way to implementing the algorithms you need to solve problems, you have to switch languages, not anything else.

catnaroek··on You can't Rust that
That's because the real value is the object identity, not its state. Trust me, object identities are immutable.
catnaroek··on Fast, Safe, and Complete(ish) Web Service in Rust
Weird, for me it is exactly the other way around. The syntax might be familiar, but it is idiotic. On the other hand, the semantics is exactly what I have always wanted C++ to be.
catnaroek··on Tail Recursion Elimination (Guido van Rossum, 2009)
Stack traces are not a very good argument. As a general rule, you should write your programs to be flawless. Only if you are unsure about a particular piece of code, then can you manually add debugging information, at your own expense.
catnaroek··on Tail Recursion Elimination (Guido van Rossum, 2009)
Not a very good example. The n-th term of the Fibonacci sequence can be computed straightforwardly with an imperative loop:

    def mul(a,b,c,d):
        x = a*c + a*d + b*c
        y = a*c + b*d
        return x,y
    
    def fib(n):
        a,b,c,d = 0,1,1,0
        while n:
            while n % 2 == 0:
                c,d = mul(c,d,c,d)
                n //= 2
            a,b = mul(a,b,c,d)
            n -= 1
        return a
IMO a better example is a loop where a producer and a consumer interact in the following way:

(0) Get an element from the producer. If the producer is depleted, exit the loop.

(1) Put the element into the consumer. If the consumer is saturated, exit the loop.

With mutual recursion, you can do something like:

    (*  Standard ML syntax. This can be easily adapted to other languages.
     *  
     *  Naming convention:
     *  
     *    is: input stream
     *    os: output stream
     *    il: input leftovers
     *    ol: output leftovers
     *  
     *  Input/output are as in C++'s input/output iterators. In fact, this
     *  code is essentially C++'s std::transform, except not retarded.
     *)
    
    structure I : INPUT_STREAM = ...
    structure O : OUTPUT_STREAM = ...
    
    datatype result = Depleted of I.leftovers * O.stream
                    | Saturated of I.stream * O.leftovers
    
    fun first (is, os) =
      case I.fetch is of
          I.Cont (x, is) => second (x, is, os)
        | I.Done il => Depleted (il, os)
    
    and second (x, is, os) =
      case O.store (x, os) of
          O.Cont os => first (is, os)
        | O.Done ol => Saturated (is, ol)
Note how either `first` or `second` can break the loop by simply not calling the other. With an imperative loop, you have three choices, none of them pleasant:

(0) Assume that input streams cannot be depleted.

(1) Assume that output streams cannot be saturated. This is what C++'s `std::transform` does.

(2) Use an ugly “break” in the middle of the loop.

catnaroek··on Is Ada a failed language?
This suggests trying “scared that my wife and children will be executed if I do it wrong”.
catnaroek··on Is Ada a failed language?
> What do you do about programming errors you cannot identify?

A programming language designer can only do so much for you. If you cannot identify errors in your programs on your own, consider switching to a different career.

> And what about programming errors that your process and your language causes?

Rigorous reasoning cannot possibly cause any problems. Rigorous reasoning helps identify and fix problems more often than not caused by prior non-rigorous reasoning.

> Programming language design is all about interactions with humans

Humans who need to reason rigorously in order to do things right, hence need to be pushed in this direction.

catnaroek··on Is Ada a failed language?
It's an empirical fact that people use languages they couldn't have designed themselves. Designing a safe language requires mathematical sophistication. Using a safe language - less so.

As for cowboy programming, Java and C# don't support it all that well, and they are very popular.

catnaroek··on Is Ada a failed language?
Last time I checked, psychology doesn't affect how a computer program runs.
← PreviousPage 2 of 34Next →