Sky-lang and vibe coded languages

I found this elm-inspired language on my research on vibe coded languages:

I know there have been various attempts to recreate other elm-lang-like languages and I have also personally updated the elm-compiler with claude code to produce optimized code and to tweak the language itself.

{ model | value.sub.sub.sub = 1 }

-- or

{ model | value.sub.sub.sub <- \x -> x + 1

-- or

{ model | value.sub.{
   value = 1, value = 33
}}

in order to reduce boilerplate code … maybe this could be an extension for the next version of elm :wink:

However #1, AI makes it quite simple to reason about language concepts instead of dealing with compiler and syntax details. If you check some other resources of the sky-language, the developer seems to be right. When AI becomes a developer it is better to use languages that give you more guarantees and better compile time errors. Not a Python fix that breaks something else somewhere. Elm could become more dominant, if it would also compile to nodejs with access to the file system …

However #2, what is also interesting about the sky-language is, that it does not deal with the idea of “defining” a single program that runs on one machine, where you have to hardwire a communication a database, program the database, etc. It is more about infrastructure. So what I was thinking about, was if we could reinterpret programming similarly. If I think about a program, then distribution is particularly of interest for me, but there is lots of stuff and effort that I have to program in order to establish a communication between nodes, how data gets serialized, synchronized, etc. I was just thinking, could something like this, not also be handled by an elm-like language or by the next version of elm, since there is a new release :wink: I was thinking about something like this, if you define a chat app:

type alias Model =
    { settings: Persistent Settings
    , editor: Shared Eventual (Lww String)
    , users : Shared Eventual (AddWinsSet UserId)
    , messages :
        SharedPersistent Causal
            (Sequence MessageId Message)
    }

During compilation, you can specify one or multiple communication options and the compiler chooses one that fits to your definitions about consistency, etc. Thus, programming is still about single programs with a “single-threaded logic”, but the result would be distributed with guarantees.

I am just thinking out loud :wink:

2 Likes

Maybe not exactly what you’re describing here, but have you seen https://lamdera.com/ ? I feel like it’s almost what you are refering to. It’s a great (dare I say amazing?!) way to write Elm code without worrying about the “infrastructure”, as you call it, but with a model of network / distribution baked in that is really nice to reason about. Highly recommend checking it out, it’s really fun!

Apart from that currently not able to look into Sky or the linked video, so can’t comment on that, sorry :sweat_smile:

1 Like

Have you looked at https://elm-pages.com/ or https://gren-lang.org/? Both compile to Node and allow for interacting with the file system.

I know lambdera, as far as I know it uses one setup only not the same as the infrastructure … I have always to rely on CRDTs for my distributed setups. CRDTs like Yjs or Automerge are divas still generating conflicts, which sometimes behave somehow strange … I was just thinking if this communication overhead also with different types of consistency and communication layers could be handled by a language in the future …

Yes, but not tested by myself so far … I was just thinking about other concepts than “classical” programming … such that if a variable is defined as persistent, then it is automatically store and restored and if it defined as shared, then it will be automatically synchronized between peers with different guarantees … I was just thinking how the next version of elm or another language might look like …

There are research languages that have experimented with that style of annotation. (I’m on holiday and don’t have access to my notes so can’t find a reference, but I’ve definitely seen a presentation about it).

But the general area is multi-tier languages, where you describe a single program, but can distribute across multiple areas. Perhaps the most promising in this area is Unison, but there have been many different attempts.

I’m not 100% sure why none of them have gone mainstream, but I suspect it has to do with many of them trading operational flexibility for productivity. Productivity ends up being easier for large organisations to buy (you hire more people or you ship later), whereas operational flexibility can be fine for a while and then a huge problem when you need it (for a taste of that you could see the same thing when Elm 0.19 removed kernel code - a lot of people left, even though they didn’t have a usecase that couldn’t be solved at the moment; but they worried that a usecase would present itself in the future and they would not have a response).

2 Likes

I know Unison from a couple of talks, but I don’t know how closely related it is to Erlang or Elixir — probably not very closely, just as many people say LiaScript is like a Jupyter Notebook, even though it isn’t.

However, for the past week, I’ve been using Claude to run benchmarks and experiments on the Elm compiler. It has added many measurable optimizations to the generated output.

That makes me think that developing a programming language may no longer be such a huge undertaking, at least in terms of implementation — parsing, optimization, translation, and so on. This does not mean that language design has become easy, but perhaps the implementation cost is no longer the main obstacle it once was.

We, as a community, could discuss the syntax, runtime environment, features, new paradigms, and potential applications, while AI could help make the implementation performant, run extensive tests, and fix the issues it finds.

I wouldn’t say there’s any relationship between them. Elixir and Erlang are typical programming languages in that they don’t have any built-in infrastructure-related features.

I can confirm this with my work on eco (elm compiler optimized). I did not actually intend to use AI when I started, but I also thought its a good opportunity to learn to do AI assisted development, if it is good enough. That was around the time Claude Code Opus 4.5 came out, and after a few experiments I decided it was actually good enough to be useful.

At one point I estimated how much work I have done in the last 6 months and it came out to around 2.5 to 3.5 years of manual work.

There were a couple of days long debugging sessions to track down some difficult heisenbugs, that I think would have taken weeks but were resolved relatively painlessly by running CC with enough privileges to use bpftrace.

I would say the implementation effort is still high. For me, I find that working on something more intensively helps to keep my motivation up, and helps me to keep the details current in my own mind. So 3 years work in 6 months is a better way for me, than 3 years work very part time spread over 3 years. But we are all different, and your optimal way of working might be quite different to mine.

Parsing, optimization, translation - it can all be done. Opus is good enough, and Fable is truly astonishing. You can give it a paper on some optimization technique, discuss and refine a design that will fit your compiler, plan out the work, and have it implement and test it all. But you can’t really just do it blindly, you need to understand the techniques at least well enough to know what is likely to work and what is not, how to prioritize work so it lands in the right order, and to know when it is not working out and make critical judgements about how to proceeed in that situation.

1 Like

I was just thinking about the idea, that functions can be updated at runtime, also databases can migrate when the system is live (in erlang/elixir)… Not that the languages are equal, but only some concepts that separate them from other languages … but again, I can be totally wrong about this :wink:

You can do that with many (most?) languages!

You can even do it with natively compiled languages (though it’s a bit trickier than with VMs). Facebook hot-upgrades their Haskell services, injecting new code into the program while it’s running, and a former employer of mine developed an IDE that would upgrade a running C++ program while it was running in development.

The thing that makes the BEAM unusual is how much tooling and support it has for hot upgrades, including an approach to upgrading stateful code.

Wow. I love it, there are all the good things.

I was not talking about the Hot Reloading for faster development, but on Hot Upgrades since from my point of view these are two totally different concepts … However, my initial idea was, that with the help of AI, we could start discussing on introducing new concepts to elm. Stuff that might be already included in other languages or something that is totally different… I thought that distributed data types with a runtime that connects everything between different peers might be a good idea. Since I mostly work in that area and know the pain points …

I would argue, that the implementation @rupert has mentioned, has become much more simple than before. I made a personal fork of the elm-compiler and updated the syntax slightly, and let the AI become a researcher for possible optimizations, something I could simply not do and not within 2 weeks …

Here is a little benchmark of some optimizations:

I let the AI inspect the generated JS code, while it generated and compiled elm by itself, then it made suggestions about possible solutions for findings, it changed the js by its own and then compared the before and after versions … if there was a significant gain about 5% it modified and tested the compiler … pretty easy and also impressive …

Additonally there are two syntax changes, that I found handy … nested record updates like

{ model | record.sub.sub.value = 12 } or { model | record.sub.sub.value ← add 1 } or

{ model | record.sub.sub.{ value = 12, valueb = 13, string = “well” }}

and the other thing is using workers … so that there is a new module Worker,

Without a worker (normal):

result = ackermann n   -- runs directly on the main thread

If this triggers a stack overflow, the whole tab crashes — and it’s frozen solid the whole time leading up to that, nothing responds. At least with elm 0.19.1

With Worker.run:

Task.attempt GotResult (Worker.run ackermann n)

ackermann n now runs on a real separate browser thread (a Web Worker), not the main thread:

  1. Main thread stays free — the page keeps responding while the computation runs in the background.
  2. Stack overflow gets caught — the worker wraps the call in try/catch internally and sends the RangeError back as a normal Err "RangeError: ...", instead of taking the page down. You just get an error value in update (GotResult (Err message)), like any other failed Task.

Now with Worker.batch:

Worker.batch
    [ Worker.run ackermann 9
    , Worker.run ackermann 9
    , Worker.run ackermann 9
    ]

Worker.run alone gives you one background computation. Worker.batch takes a List (Task x a) and runs them concurrently across a whole pool of workers (one per CPU core by default) instead of one after another — unlike Task.sequence, which would wait for the first to finish before starting the second. You get back a single Task x (List a) with all the results once every one of them is done.

So: Worker.run = “don’t block the main thread, and fail gracefully.” Worker.batch = “and if I have several of these, run them in parallel instead of queued.”

All of this, I could not have developed … Again … using AI we could start of discussing and introducing new concepts … maybe even something like an elm standard for different eco systems like eco :wink: with their own libraries and compilers … but all aggree to use some of the same core modules, principals … however :wink:

Aye! I gave 2 examples of hot loading code in a production application, and 1 example of it in development.

From a technical point of view they are much the same thing, in my opinion. The main difference I think would be how the programmers and system administrators work with the technology.

You could look at elm-optimize-level-2 and port all its optimizations back into the Elm compiler too.

Another fusion one you could look at is elm/bytes. Since Bytes.Decode.Decoders and Bytes.Encode.Encoder are both opaque, you can think of them as a sealed DSL. All that matters is the API, the compiler can substitute an alternative implementation but retain the same API contract and all consumers of elm/bytes will continue to work as before. Then do fusion on all the encode/decode ops, the main win here being the fusion of bounds checks over multiple ops.

VirtualDom.Node is also a sealed DSL, which suggests some interesting ideas that I have not explore yet…

For eco my plan is to make it 100% Elm compatible - so no language forks. Only new APIs under a different kernel space eco/* rather than elm/* . I think fully supporting all of elm/* is the best way to avoid fracturing the existing ecosystem.

I think your Worker.* package could fit into that model ?

That is a really convenient extension of record update syntax. I do want to point out that it makes it easy to update nested data which might encourage violating the Law of Demeter or avoiding encapsulation. We may not always want to make a thing easier (a la Rich Hickey’s talk Simple Made Easy). I think Elm’s design probably involved very precise opinions about what should be easy.

I know that at one point Evan said something to effect that if he could make optics impossible in Elm he would do so. I believe that is because he believed optics encouraged bad design because the motivating example everyone tends to give for optics is “Look how easy it is to reach five levels deep into this data structure!” which, in my opinion, is kind of like saying “Look how easily I can shoot myself in the face!”. The thought is that it is maybe better to use opaque data types and have functions that return Result to preserve invariant conditions, encapsulate concerns, etc. etc. etc.. Or even if we don’t use opaque data types at least use functions so that our types are more robust and meaningful (less anemic).

Anyway. I don’t know. I could be wrong about what he thought. Or maybe I imagined that he said that thing about optics.

I personally believe that reaching deep into records

  • is sometimes appropriate
  • but often overused and inappropriate
  • and I believe that optics actually provide a solution!

Given an opaque data type that wraps a record with 5 fields, I might have 2 fields that can just be mutated individually where I don’t need to maintain integrity or invariant checks. I am completely cool with outsiders reaching in and updating those two fields directly. However, I might have three fields that have to maintain some kind of relationship with one another. I’ll expose lenses for the two fields I don’t give much of a care about whether they get updated and then I’ll expose a bunch of domain operations which may or may not fail which preserve the invariant conditions between the other three fields.

In a sense

  • Exposing an optic is like declaring a property has a getter and setter in C#.
  • Exposing only an accessor function is like declaring a property has only a getter (read only).
  • Declaring neither an accessor nor an optic means completely hidden.

The problem is that optics are often a nightmare for new engineers to learn. Also, Profunctor optics in PureScript are beautiful and elegant and expressive and yield error messages so horrible you will want to quit development altogether and go into hiding. I’m not kidding. The error messages are terrible.

So a steep learning curve and terrible error messages does not really sound like the Elm way!!!

However, one thing that often occurred to me when working with profunctor optics is how the traversal optic (with Function as the Pro-Functor) captures about 70% of the ergonomic value of using optics. That is basically the standard composable updater function:

type alias Updater larger smaller = (smaller -> smaller) -> (larger -> larger)

So if one has

type alias Model = { person : Person }
type alias Person = { addresses : List Address }
type alias Address = { line1 : String }

and we have

updateLine1 :   (String -> String) -> Address -> Address
List.map :                           (Address -> Address) -> List Address -> List Address
updateAddresses :                                           (List Address -> List Address) -> Person -> Person
updatePerson :                                                                               (Person -> Person) -> Model -> Model

we can compose them to get a function that will update line1 of an address for each address inside the person inside of the model (String -> String) -> Model -> Model.

We can also write updater functions for variants in a custom type so long as the variant has a single argument. So if we have

type Fruit = Apple AppleInfo | Banana BananaInfo

we can write

updateApple : (AppleInfo -> AppleInfo) -> Fruit -> Fruit
updateApple f fruit =
   case fruit of
     Apple appleInfo -> Apple <| f appleInfo
     _ -> fruit

So this works for updating both custom types and records so long as the update is pure (cannot fail).

So one thing that occurred to me is that a language syntax extension - spiritually connected to the .FIELD_NAME - would be

  • %FIELD_NAME - Generates a function of type (a -> a) -> { r | FIELD_NAME : a } -> { r | FIELD_NAME : a}
  • %VARIANT_NAME - Generates a function of type (a -> a) -> CUSTOM_TYPE -> CUSTOM_TYPE only where VARIANT_NAME names a variant in CUSTOM_TYPE with exactly one argument.

Then this syntax can be used and compares somewhat favorably with yours

{ model | record.sub.sub.value = 12 }
(%record << %sub << %sub << %value) (always 12) model

{ model | record.sub.sub.value ← add 1 }
(%record << %sub << %sub << %value) (add 1) model

{ model | record.sub.sub.{ value = 12, valueb = 13, string = “well” }}
(%record << %sub << %sub) 
    (\whateverSubIs -> { whateverSubIs | value = 12, valueb = 13, string = "well })
    model

The nice thing is that it is just functions. Nothing actually tricky.

Now if we wanted to get more custom in the syntax we could simplify the composition and say that as soon as a % is encountered that we will start a path of field name | variant name and produce the corresponding function. That would look like

{ model | record.sub.sub.value = 12 }
%record.sub.sub.value (always 12) model

{ model | record.sub.sub.value ← add 1 }
%record.sub.sub.value (add 1) model

{ model | record.sub.sub.{ value = 12, valueb = 13, string = “well” }}
%record.sub.sub 
    (\whateverSubIs -> { whateverSubIs | value = 12, valueb = 13, string = "well })
    model

Again, it would work for variants on custom types as well so one could write

%person.contactMethod.PhoneContact.isDaytime 
-- : (Bool -> Bool) -> Model -> Model

which would update the person, by updating their contact method, if the contact method was PhoneContact, and it would transform the isDaytime : Bool field in a record of PhoneContact.

I am not saying this is something that should be done.

I do think it is a clever idea that I thought people might enjoy a brief discussion of

  1. The relative value of convenience in language design.
  2. Encapsulation, invariant enforcement, and Law of Demeter as it pertains to this.
  3. Type simplicity and surprising power of the traversal optic. I always thought it was kind of cute.
  4. My silly % syntax idea.
2 Likes

elm-optimize-level-2 were the first optimizations claudIA had to integrate, but then there are others like list-pipeline-fusion:

If you take a look at the benchmarks, the time for the new list pipes are nearly constant, since only one single loop is generated, this is not done by elm-optimize … next to other findings … But again, for a single person or a single team, such optimizations are hard to identify and even harder to implement …

I did not tackle modules so far, only the compiler, but I think the virtual dome is pretty perfect …

So Eco does not rely on kernel modules?

The worker itself is implemented as a new kernel module. The compiler checks that only known functions can be passed to it, since those functions must also be available to the worker after compilation. It also automatically adds the required serialization.

Looking at it now, it can already handle union types and, according to Claude, basically any Elm data type. :wink:

I am really just experimenting, and it is fun to play around with different ideas and concepts for a programming language. This is also the idea behind Sky: generating a new language with a bulletproof compiler, so that it becomes much harder to produce sloppy code later on.

Again, I am only experimenting, but I think AI makes it much easier to explore new directions in programming-language design—instead of forcing the AI to generate code that has to deal with both the beautiful and the ugly aspects of an existing language.

From my point of view, Elm is beautiful, but in some respects it can also be a bit of a diva. :wink:

Don’t know if this makes sense X-)

It does. It has C++ translations of most of the elm/* kernels. And then its own eco/* kernel for backend stuff like reading files and so on.