The Koka language uses a similar approach to track resource usage, except there they use ref counting and just remove unnecessary ref counting operations. Neat stuff.
> Here, we'll see how a type is translated into a function that discards/copies the terms of the type. To see the basic idea, let's take a simple ADT for example:
data item {
| New(int, int)
}
> The internal representation of New(10, 20) is something like the below:
> New(10, 20)
// ↓ (compile)
let v = malloc({2-words}) in
store(10, v[0]);
store(20, v[1]);
v
I suspected that it's not actually heap-allocating every single bit of memory in every program, and from looking around more in the docs, I _think_ the "Allocation Canceling" section here explains what I was missing (https://vekatze.github.io/neut/basis.html#allocation-canceli...):
> When a free is required, Neut looks for a malloc that is the same size and optimizes away such a pair if one exists.
This is a really interesting way of automating memory management at compile time. I imagine there's still a lot of room for different choices in this strategy (e.g. choosing to reuse part of a larger allocation rather than looking for one that's exactly the same size and then leaving behind the remainder to re-use for a future allocation), and I'm super curious about whether this would end up encouraging different patterns than existing memory management systems. Offhand, it almost seems like it could act as a built-in allocation buffer managed by the compiler, and I'm curious if the algorithm for reusing memory is smart enough to handle something like manually allocating the maximum amount of memory needed for the lifetime of the program up front and then re-using that for the duration of the program to avoid needing to allocate anything dynamically at all (although my worry would be that this would devolve into the knapsack problem and not be feasible in practice). If this did work though, my immediate idea would be for some sort of hook where you could specify the maximum amount of memory you'd be willing to use, which could then turn "using too much memory at runtime" into a compiler error. My assumption is that that I'm missing something that would make all of this not work the way I'm thinking though.
> I'm curious if the algorithm for reusing memory is smart enough to handle something like manually allocating the maximum amount of memory needed for the lifetime of the program up front and then re-using that for the duration of the program to avoid needing to allocate anything dynamically at all
I imagine this would be equivalent to solving the halting problem. So you’d need to restrict the language in some significant ways. I’d definitely still be interested.
There might even be some possibility for cross-pollination with ideas such as Counting Immutable Beans [0].
I’ve long wondered if there could be a functional programming language that made static memory management less verbose and easier to manage without monads and other explicit structures.
Could someone explain the “Necessity and noema” section [1] or share a reference? Looked like it might be significant but I couldn’t make much sense of it
TL/DR: All functions are pass-by-value. To avoid complete tanking of performance, they have "noema" (same as a reference in other language), which contains pointer to "hyle" (reference target in other languages). Since the language is GC-free, the references cannot escape out of the block they are defined in.
The language authors really like inventing the new programming terms.
It looks partly like OCaml, with the "let ... in" kind of syntax. Also the "unit" word. I think in OCaml it means a function that doesn't return any value, but why is the word unit used for that?
This comes up in languages where everything is an expression, nothing is a statement. Because everything is an expression that needs to be evaluated, everything has a return type. You can't have something that doesn't return literally nothing (void), you have to return an empty expression. Thus, unit.
Requiring all language constructs be expressions and eliminating statements means that you avoid a lot of duplicate effort in the language design.
For example, in C-like language you have if-else statements and ternary expressions. The ternary expression does "the same thing": as condition statements, but it also evaluates to a value. So in functional programming languages, you just have the one kind of conditional expression, and then maybe some syntactic sugar to morph it into more ergonomic forms.
It's more precise to think of unit as an empty tuple. A tuple is like an ad-hoc struct without names for the fields, so `(int, string)` is like `struct MyTuple { int a; string b; }`. An empty tuple would be `()` (which is the syntax for unit in many languages), meaning `struct MyTuple {}`. If you think about it, that's about the closest you can get to "nothing" without invoking type system handwavium (like void, which is basically "unit without the good parts but that can also mean anything").
You can do clever stuff with it, for example `HashMap<string, ()>` is practically identical to `HashSet<string>` (depending on how clever the compiler is, possibly actually identical).
I don't think it's particularly useful to think of unit as an empty tuple specifically, that is just an arbitrary but convenient definition for it.
Really a unit type is just one that contains only a single value. This is a unit in the same way that 1 is a unit for the integers. With some hand waving it is an identity for product types, for example (int, ()) is the "same" (xxxmorphic yada yada) as int
>you think about it, that's about the closest you can get to "nothing"
Some other options could be to use None (like Python does) or Nil or Nothing itself, or even ReturnsNothing to be more explicit, or even the Pascal-style procedure keyword, instead of the function keyword, for a sub routine that returns nothing.
For "How Fast is This?" it links to a benchmarks page, which only shows that it's faster than Haskell. It would be more informative to instead compare against a language that is more popular and/or more performant than Haskell.
C is usually 1-4x faster than Haskell. This looks to be within 1-2x.
The key takeaway for me is that you're in the range where very slight variations in implementation of the same algorithm make the difference between which is faster.
I'm currently reading through the automatic memory management claims which look really cool (reminds me of linear types), but the highlighted punctuation (, : =) makes it very painful to read.
Show HN: A dependently-typed programming language with static memory management - https://news.ycombinator.com/item?id=23283880 - May 2020 (78 comments)
I'll see if I can email the author.
> Here, we'll see how a type is translated into a function that discards/copies the terms of the type. To see the basic idea, let's take a simple ADT for example:
> The internal representation of New(10, 20) is something like the below:> New(10, 20)
I suspected that it's not actually heap-allocating every single bit of memory in every program, and from looking around more in the docs, I _think_ the "Allocation Canceling" section here explains what I was missing (https://vekatze.github.io/neut/basis.html#allocation-canceli...):> When a free is required, Neut looks for a malloc that is the same size and optimizes away such a pair if one exists.
This is a really interesting way of automating memory management at compile time. I imagine there's still a lot of room for different choices in this strategy (e.g. choosing to reuse part of a larger allocation rather than looking for one that's exactly the same size and then leaving behind the remainder to re-use for a future allocation), and I'm super curious about whether this would end up encouraging different patterns than existing memory management systems. Offhand, it almost seems like it could act as a built-in allocation buffer managed by the compiler, and I'm curious if the algorithm for reusing memory is smart enough to handle something like manually allocating the maximum amount of memory needed for the lifetime of the program up front and then re-using that for the duration of the program to avoid needing to allocate anything dynamically at all (although my worry would be that this would devolve into the knapsack problem and not be feasible in practice). If this did work though, my immediate idea would be for some sort of hook where you could specify the maximum amount of memory you'd be willing to use, which could then turn "using too much memory at runtime" into a compiler error. My assumption is that that I'm missing something that would make all of this not work the way I'm thinking though.
I imagine this would be equivalent to solving the halting problem. So you’d need to restrict the language in some significant ways. I’d definitely still be interested.
I’ve long wondered if there could be a functional programming language that made static memory management less verbose and easier to manage without monads and other explicit structures.
[0] https://arxiv.org/abs/1908.05647
[1] https://vekatze.github.io/neut/terms.html#necessity-and-noem...
TL/DR: All functions are pass-by-value. To avoid complete tanking of performance, they have "noema" (same as a reference in other language), which contains pointer to "hyle" (reference target in other languages). Since the language is GC-free, the references cannot escape out of the block they are defined in.
The language authors really like inventing the new programming terms.
Requiring all language constructs be expressions and eliminating statements means that you avoid a lot of duplicate effort in the language design.
For example, in C-like language you have if-else statements and ternary expressions. The ternary expression does "the same thing": as condition statements, but it also evaluates to a value. So in functional programming languages, you just have the one kind of conditional expression, and then maybe some syntactic sugar to morph it into more ergonomic forms.
You can do clever stuff with it, for example `HashMap<string, ()>` is practically identical to `HashSet<string>` (depending on how clever the compiler is, possibly actually identical).
Really a unit type is just one that contains only a single value. This is a unit in the same way that 1 is a unit for the integers. With some hand waving it is an identity for product types, for example (int, ()) is the "same" (xxxmorphic yada yada) as int
Some other options could be to use None (like Python does) or Nil or Nothing itself, or even ReturnsNothing to be more explicit, or even the Pascal-style procedure keyword, instead of the function keyword, for a sub routine that returns nothing.
But seriously, according to that link, it seems to me like the zero or empty type is more suitable.
But I am not a PL or type theory expert.
The key takeaway for me is that you're in the range where very slight variations in implementation of the same algorithm make the difference between which is faster.
Deleted Comment
Still no, thank you, doesn't seem to be as compelling as Rust to me.
Deleted Comment