Skip – A programming language to skip the things you have already computed
skiplang.com
skiplang.com
By the way it seems very sad to me that the majority of imperative and hybrid (functional×imperative) languages lack syntax for immutable variables: in just so many cases I introduce a variable to store an intermediate result of an operation an don't mean to re-assign it ever after, it's nice of a developer to define this intention explicitly and of a compiler to raise an error when the variable is re-assigned accidentally - this simple feature is among the reasons why Scala programs usually are comparably easy to debug and run as expected as soon as they get compiled successfully.
The big challenge is how do you maintain those guarantees while having a language that product engineers would want to write in.
You can decide to not allow mutability in your language but developers need to think about code in a very very different way than what they do now. It’s also going to be challenging to convert/translate existing codebases and patterns to it.
So we decided to allow both mutability and immutability. But it poses very interesting challenges. For example in the standard library, what is the type of x.map(y -> z). Which pieces are mutable or immutable. If you are not precise enough in your type system, then you are going to type it as “i don’t know (readonly)” or “mutable” but then you can’t pass the result of this to a memoized function, so you lost the point of the language.
The other interesting aspect is that the backend relies on the fact that the invariant are true in order to output optimized code and garbage collector.
So we cannot “cheat” like many gradual typed languages like Hack, TypeScript, C# where they can just say, actually this type is wrong but i’m just going to ignore it and it’ll throw an exception at runtime if the programmer got it wrong. It would segfault in Skip case.
For Skip to have to know that the actual instance is immutable, and that no one has a reference to a mutable version of that instance. If the inputs/outputs change after the memoization, we cannot guarantee the correctness of the reactivity/cache-invalidation. Thus for memoizing the inputs/outputs to a function, we need to know that the object, and any of its fields, will never be modified. In other words, we need to know if the object is transitively immutable. In order to get this sort of analysis, you need the type system to provide the information, and it is not enough to have a simple local analysis.
[[This was written by Todd Nowacki but his new HN account has been flagged as commenting too fast...]]
PS: Whatever, it pleases me that somebody has finally come to the idea of a heavily-memoized language that would still support mutable imperative parts and tried implementing it. I've been thinking "why the eck there is no language that would memoize pure functions automatically" for so many years already (but I've always been dismising the idea of building it myself as I lack sufficient background in computer science to build a compiler that would produce reasonably fast programs so it would be impractical to invest time). The only language I've found easy to add automatic memoization to is Python.
PPS: That's a pity Todd Nowacki has been flagged, HN should really learn to distinguish between garbage fast commenters and qualified resourceful writers.
> but can't an object be considered immutable when all its fields are immutable and all its member functions (but the constructor perhaps) are pure?
How exactly do you expect to track this accurately without integrating it into the type system? It's not an issue of whether or not the field itself is assignable, but whether or not the type that it points to is mutable. This becomes much more difficult to determine when generics are involved. This sort of type level knowledge for generics would make it hard to staple onto an existing type system. Even if you did, you would likely end up with a lot of code duplication as you would need two forms of types, immutable and mutable, e.g. `ImmVector` and `MutVector`, `ImmMap` and `MutMap`, `ImmMyObject` and `MutMyObject`, etc. To make this more ergonomic, Skip has a mode based mutability model, where the mutability is determined not at the class declaration, but rather at the type annotation/object instantiation. So we would then have `Vector` and `mutable Vector`, two modes of the same class.
You can read more here: http://skiplang.com/blog/2018/04/10/understanding-mutability...
The hashmap version of immutable maps are really useful for the "builder pattern". This is very common in product code. You create a mutable local map by slicing the inputs in many ways and freeze it at the end. We have an optimization that makes freeze a no-op if we can prove that there are no references to the variable that escape (it's true in many cases). We often talked about doing a compaction step at this point but haven't played with it.
The tree version of immutable maps are really useful when you are mutating (hmmm...) it after it escapes the function.
The problem is that the two have a very different API and complexity trade-offs. We haven't found a way to unify the two APIs and have the compiler able to pick one or the other transparently behind the scenes.
Also, one thing the language tries to have is predictable performance. Having a different complexity based on whether an optimization is kicking in or not is something that bit us many many many times on the dynamic languages we're working on (Hack, JavaScript, Python) that it is something we're trying to avoid with Skip.
Ideally, in a OO language, what I want is to be able author a single class, but then "overload on immutable", so to speak. So there are some class members that are shared for both implementations, and then there are some that are only there when the object is mutable, or is immutable (in the most extreme case, there are no shared data members or method implementations at all, and only the API is common).
That would imply that a class definition effectively implicitly defines a trait derived from the common members - e.g. "Vector", having operations such as "size" or "[index]" - and then two distinct classes, e.g. "mutable Vector" and "immutable Vector", both implementing the trait, and providing some extra APIs where that makes sense.
This would be helped a lot by a full decoupling of classes from types, as some modern (and not so modern - e.g. OCaml is practically a golden standard here) languages do.
struct Foo {
A bar(); // If the instance is non-const.
A bar() const; // If the instance is const.
};
Of course this is C++ so it's up to you to make sure that all the types contained in your object are also immutable when they're const-ed (e.g. avoid pointers). And the mutable keyword is a backdoor.Or perhaps you wanted to be able to change the representation (e.g. member variables) of your type based on whether it's mutable or not? Although in that case I'd probably go with an interface.
As far as changing representation, that's exactly what I meant by "class members" (and why it doesn't say "class methods"). An immutable map should have a different internal representation from a mutable map, for example, to permit for efficient copy-with-update. But logically, they're both maps. C++ punts on this problem - a map is a map, and the only thing that "const" does is make its data immutable, which in practice means that immutable containers aren't used much because there's no way to do copy-with-update (e.g. given a const std::list, you can't efficiently create a new const std::list with an item prepended, like you can in Lisp or ML - you have to create a whole const std::list, copying all items from the original one).
Languages that tried to tackle this basically just came up with a pattern where you just write two completely different classes, that have a common pattern in the name by convention (like Map and ImmutableMap), and implement a common interface/trait. But there's no link between those two classes other than convention, and the common interface has to be authored manually, even though it can be derived entirely from the two classes in practice.
So, what I'm proposing is basically a better way to write Map and ImmutableMap in a way that would 1) make it clear that those are semantically related, even though they're distinct in implementation, and 2) automatically derive the common trait. So when I write a function, I can say "takes a Map", and that's a trait that covers both "mutable Map" and "immutable Map" - automatically!
val buf = scala.collection.mutable.ArrayBuffer.empty[Int]
buf += 1
is perfectly valid. You can't reassign buf to another object, but you can change the underlying object. Making something "truly" immutable when that's allowed is tricky. Apache Spark (built on top of Scala) goes a long way to try and achieve this, and is able to do a number of optimizations as a result, but IIRC there are still significant cases it isn't able to resolve.The other approach is to do deep copies but it’s very expensive in practice.
Kinda like C++'s const, except that's actually "readonly" (which is also handy to have!), while what we really need is "immutable". Or rather make immutable the default, and then "readonly" and "mutable" have to be explicit everywhere.
If a non-trivial program runs as expected on the first try I get really suspicious.
Why don't you just use the functools.lru_cache decorator in the standard library?
also just shared it here https://news.ycombinator.com/item?id=18080598 if you have comments
I am quite frankly speechless that anyone could hold this opinion.
When you want to memoize a function, then we move all the arguments to the intern heap which makes structurally equal values have the same pointer. Then we can figure out quickly if two immutable values are equal.
We have an intern() function that lets you move a value to the intern heap manually. We are also thinking about adding a field for objects such that once it is interned somewhere, if you intern it again, it's a no-op and we use the pointer in the object. This would avoid accidentally increasing the complexity of an algorithm by repeatedly interning the same value.
I wholeheartedly disagree. Scala is among the most difficult languages to master. From experience, a successful compilation do not shield you from bugs and runtime exceptions. NullPointerException being my favourite, as a reminder that lots of libraries are built on top of Java's ecosystem, and thus plagued with the same curse.
And even if you're toying with pure Scala code, without any dependencies, it's still possible to end up in a state where the given traceback would be absolutely useless and full of (Anonymous) calls. Good luck with that.
I would very much like to see this as an area of active development both in programming languages and distributed computing platforms. Current streaming platforms are great and it's getting easier and easier to create declarative data flow pipelines, doing real-time processing, splitting things into windows etc. But on top of that I imagine incorporating incremental computation, allowing you to keep a durable history of the event stream (or at least manageable parts of it) in a way that automagically allows incremental recalculation of upstream data when an underlying fact is retracted or updated.
Reading the 'when not to use Timely Dataflow' section on sorting, are there any ideas from Nominal Adapton that are relevant in this distributed setting, when we're talking about small updates or inserts?
https://eurosys2017.github.io/assets/data/posters/poster21-Lattuada.pdf
Which Kafka link is dead? The work has quiesced because any next step seems to be involve assuming something about timestamps in the input. At the same time, it seems to take about 10 lines of code to write a Kafka source or sink, minus any careful worrying about acks for durability.The DD and Adapton approaches may be a bit tricky to hybridize. Much incremental compute works by moving through a sequence of valid configurations, and DD's main departure is generalizing this to partial orders. So, maybe you could borrow ideas, but it would probably be original research to do so.
Email is great, as it is def no longer about skiplang.
Just a warning if you're interested in using this.
"The language, compiler and libraries are maintained as a side project by Julien Verlaguet, the main designer of the language."
FWIW I don't think anyone is claiming you should, especially given it's not under active development (:
And I am looking for people to help ;-)
> The Skip project concluded in 2018 and Skip is no longer under active development at Facebook.
This makes it sound like the project was killed off entirely, as it mentions its 'concluded'.
Converting a multi-million-line codebase takes many years, and in practice, it turned out that huge swaths of the code would need to be converted before any wins were had. (To fully leverage reactivity, all your dependencies also need to be reactive.) Our codebase is intertwined and doesn't have many self-contained parts that could be fully converted in isolation so this wasn't possible.
Essentially, Skip is still very promising as a language for new projects but it feels like Facebook management couldn't justify staffing a permanent team given that we don't have a path to move huge chunks of development to it except in projects being developed more from scratch, which we tend to have few of.
I wouldn't see any of this as an indictment of Skip's potential generally.
> "I see a lot of C++ influence" I don't see what gave you that impression. Perhaps the syntax? But I don't think it looks more like C++ than Java or C#.
> Why would I choose to invest in your language over one of these other ones that has more momentum?
There are several reasons, but of course I am biased: 1- builtin cache invalidation 2- safe parallelism 3- predictable GC
While 2-3 can be found in other languages, I think I can argue that 1 is very unique to skip.
The other thing is, what are you trying to build? If you what you want to do is to develop an incremental tool, then Skip is probably the best option out there right now.
Let's say you decided tomorrow to build a fully incremental C++ front-end, to support auto-complete for example (I chose C++ because it is notoriously complex). What would you write this in? I think Skip should be the language of choice.
The interesting about Skip is that local mutations are explicit with a ! in front of it. And it works in the context of a lvalue.
We may want to rename this title if it's confusing.
Can you elaborate on this comment in the context of some of the other languages listed by the grandparent (Go, Swift, Julia, Nim)? Granted, writing bare-metal operating system kernels may severely limit practical options, but I can imagine using Go, Swift, and Nim for slightly higher level "systems programming". Skip would be a reasonable alternative to these, right?
It really depends what one means by "system programming". I would add Java/C# and many others to that list.
I think you've done a really good job with Skip. I really love the incremental type-checking. It's a breath of fresh air to see a compiler created with IDEs in mind from the beginning.
Effect tracking is something that Nim also boasts so I'm curious how that works in Skip. Are there any docs/articles going into detail about this?
For that reason Java and C# cannot be counted as system programming languages, because neither of those can interact directly with the system they're running on.
Ex. C# needs PInvoke, where as C can just call system functions directly.
At least that's my understanding.
How would you describe when to use Skip? It looks really interesting.
The GC and the heavy weight runtime make it unsuitable in this space.
Skip seems very similar to Swift and Java, with inspiration from Rust wrt mutability.
Skip supports ergonomic asynchronous computation with async/await syntax
Do you see the language supporting caching beyond in memory (i.e. plugin redis/memcached etc) and allow caching between processes?
Nice! I love this trend.
My biggest surprise was that the language has been designed by people that have been working on languages for their entire lives so it was dead simple to write the formatter for it compared to JavaScript.
It only took me ~2 weeks to get it in a place where we could reasonably convert the entire codebase to it. It took Prettier ~6 months to get to that point.
Do you think it's feasible to have a "jsfmt" tool or is the technical hurdle too great?
[0]: https://en.wikipedia.org/wiki/Axum_(programming_language)
Even back then it was very buggy, and in a normal size program, you would certainly run into some bugs. Sadly Microsoft has taken most resources regarding Axum down some while after discontinuing it, so if someone would want to try it out they would run into a lot of dead links.
Based on my fading memories (but take this with a grain of salt, since I was much less experienced back then), I would say that Axum and it's actor-focused model provided not enough upsides to warrant it being its own programming languages. I had better experiences using libraries like Akka in JVM languages, or Actix in Rust.
Other non deterministic computation, like randomness or time is outright banned in the tracked environment. (There is an opt-in 'untracked' modifier if you want to write code outside of the reactive environment).
For other tricky behavior, like mutability/mutations on an object, the type system tracks the mutability mode of any object. Only objects that are fully immutable ('frozen' in the language) can be memoized inputs/outputs to a function.
whats the difference between opening a file and watching it and watching stdin?
FIFO file descriptors, not so much.
If you can't seek backwards, you pretty much have to cache the whole contents in memory, which seems wildly inefficient.
As far as results, we saw some great numbers for the effectiveness of the recomputation. The language is self hosted. And the type checker is currently incremental. On my machine the initial type checking of the compiler itself is in the ballpark of ~40s. Changing a file and getting new type errors returns in <0.5s
Some of the interesting ones:
- An overview of how memoization works and the MVCC model behind the scenes: http://skiplang.com/blog/2017/01/04/how-memoization-works.ht...
- How pattern matching is implemented and the tricks to make goto work in JavaScript: http://skiplang.com/blog/2017/11/15/simulating-goto-in-javas...
- The work done on making error messages much more helpful by understanding common idioms from other programming languages: http://skiplang.com/blog/2017/11/20/fixing-the-syntax-barrie...
- The macro syntax that elegantly solves a lot of use cases where dynamism is commonly used http://skiplang.com/blog/2018/07/24/macros.html
https://sw1nn.com/blog/2012/04/11/clojure-stm-what-why-how/
The gist of it is that instead of tracking multiple separate locations in memory for values, the software transactional memory (STM) references data by value. So if you assign the value {a: 42, b: 24} to two variables x and y, that value is only stored in memory in one place that the variables both point to. Then if one of the variables changes something, for example y.b = 25, this big tree structure works like copy-on-write and makes copies of branches when mutations occur. So internally, a: 42 is one reference and b: (24 or 25) is another reference. So rather than using 4 cells of memory, we've only used 3.
This frees the developer from having to micromanage memory resources and makes a lot of other things like concurrency "just work" without locks. Do I have this correct?
The practical application is having a webserver that runs a bunch of requests in parallel but that can all share the same memoization cache. We don't want to lock the full memoization cache and therefore block all the other requests when one thread writes a new value.
Certain kinds of networked games are built as a model, deterministically updated by clock ticks and commands from the server, and a view layer on top of that. And even model-level objects often monitor each other for changes. Seems like a perfect fit.
Game objects often have graph-like (not tree-like) dependencies though. E.g. two characters may want to move towards each other, creating a pointer cycle. Ideally I'd want one to be "magically" updated if the other disappears or changes state. I wonder if Skip has a good answer there, or if a good language-level answer is even theoretically possible.
The big problem with languages with GC is that it is impredictable: when the amount of allocated memory globally passes a certain threshold, then a GC pass happens and it’s unclear how much time it’ll take and you may miss your frame.
The idea of Skip is that GC always or never triggers for some functions. So if while you're developing your game/app the time it takes to GC is within the bounds you wanted, then it’ll keep behaving this way in production.
If it is too expensive for your use case, then you can optimize that specific function (and what it calls) using a profiler. You don’t have to think about the context of the entire app to figure out how to reduce GC pauses.
The other very important thing to note is that Skip has a memory model suitable for that use case. Every function has a GC overhead, but that overhead is non-contextual. Meaning, the GC will collect the memory for only one function (instead of the entire heap). This is possible thanks to the guarantees of the type-system.
Can Skip maintain such a self-referential invariant automatically? Does the answer depend on A and B's mutability?
(This "graph as array" escape hatch seems to come up in many language designs with compile-time pointer analysis. It's useful, maybe even essential, but you tend to lose some nice language features/guarantees with it, in my limited experience.)
The GC thing sounds excellent!
Bob Morgan was behind it.
[1]: https://www.tweag.io/posts/2018-07-10-funflow-make.html
We talked about doing it at an earlier phase in order to output JavaScript closer to what the input code was, in a very similar fashion as BuckleScript does. But we never gotten around to as we focused on the native backend.
We also have a prototype of a wasm backend using emscriptem.
Right now none of them are really usable as is but there's a lot of potential there.
We had a specification in the works, but it fell horribly out of date. I wasn't sure that we kept it around
If you want an overview of language features/design. I'd start with the docs: http://skiplang.com/docs/hello_world.html