Hacker News new | past | comments | ask | show | jobs | submit login
Update on the Bagel language (brandons.me)
50 points by ingve on Jan 25, 2022 | hide | past | favorite | 22 comments



I don't have anything relevant to say about your language (it's neat!), rather I'm super curious about your fantastically cool blog software. It looks like you're importing comments from hacker news directly.

1. It looks like 2. the comments 3. support markdown 1. Whoa.

> Do you have quotes?

```rust

fn foo() -> &'static str { "bar" } ```

# Headings

Maybe?

A link to [my website](https://fakeyou.com)

And maybe even an image

![](https://i.imgur.com/9qzBYbH.jpeg)

(Sorry, I just have to test this out. Your website is awesome.)


Hey thanks! :D

It’s almost all custom (you can see the source on my GitHub https://github.com/brundonsmith/website)

Most of the site is statically generated at startup using regular JS template strings, and then served as static files (cached in-memory) by Express. Posts are written in Markdown, and code blocks are processed into language-highlighted HTML using Prism.

The only dynamic bit is the HN comments, since I want those to stay fresh. My Node server queries the Algolia HN API to get all comments for the top HN link for the post (since there could be more than one), munges the data a bit, renders it as HTML, and then caches it for one minute. Some client-side JS just hits the endpoint and dumps the HTML into the page.

Any rich rendering inside comment bodies, like for code blocks and italics, actually comes directly from HN itself! The content I get has already been processed from HN’s special markup syntax into HTML, though of course the structural content around the comment bodies is custom.

Basically what I’ve found is that people often ask good questions under my posts, and the discussion/my responses end up being a valuable part of the post itself (clarification, expansion, etc), so I wanted that extra context to be available for people who don’t find my posts via HN. That’s also why I highlight my own comments in that view; I see them as a part of the blog post.

This site is my relaxing project that I tinker with occasionally, so sometimes I over-engineer parts of it a little bit for fun :)


Whoa, that showed up right away! Really cool integration.

  And of course 
  this is how you 
  do code fences.


I'm really curious to see how the func/proc distinction will work in practice because I'm all for that funcs are side-effect free to the caller but the fact that they have to be a single expression feels like it would bifrucate the style of any project into functional and procedural to the dismay of someone reading your code in the future.

The fact that I can't take something in a proc block, remove the side effects, and shove it into a func feels like effort for its own sake and makes it more difficult to use funcs when you want to maximize their use for the mental load of the caller.


Author here, there’s one feature I didn’t show in this post that makes things a lot more ergonomic: any expression (most importantly but not limited to functions) can have inline const declarations

Example:

  func foo(a: number) =>
    const b = a * 2,
    const c = b + 17,
    b * c
Between that and expressive conditionals, I think most ergonomic concerns about a single expression are covered (but I’m happy to deal with any others that come to light!)

As for bifurcating the language: sorta, but there isn’t actually much syntax that’s different between the two contexts. Mostly the use of semicolons, and then the existence of loops in procedures. And all of it on both sides is designed to be familiar to the target audience.

> The fact that I can't take something in a proc block, remove the side effects, and shove it into a func

As a reminder, procs can’t return values at all, so it’s unlikely you’d ever really have a case to do this with a whole chunk of a proc. You might want to extract an expression out of a proc and into a function, but that should work pretty naturally as-is

Edit: and I think actually, the strict policy against return values for procs probably helps with the question of bifurcation in a way. Without it, some people might just prefer to construct their values procedurally. But with this limitation, the whole team has to be on the same page: if you want to re-use logic that derives a value, you have to use a func.


I'm also curious about this func/proc distinction and have some questions. It certainly isn't a new concept. Functional languages like Haskell have a similar distinction between effectful actions and pure functions. D also has pure functions that can only call other pure functions, but I'm not sure how well that has worked out in practice. Languages like SML/OCaml make it easy to write purely functional code, but I don't think the language enforces it.

1: Is there really no way to reuse procedural code in a pure setting? Say I have a procedure

    proc inPlaceSort(a: mutable IntArray) { ... }
Is there any way to use this proc to define a func that returns a sorted copy? For example:

    func pureSort(a: IntArray): IntArray =>
        mutable array_copy = copyOf(a),
        sort(array_copy),
        array_copy
Do I have to write two separate sorting algorithms? In Haskell I can implement both efficiently with a single implementation in the ST Monad.

2: Can pure functions perform logging?

3: Can a procedure declare some parameters as immutable or as output-only?

- Immutable parameters: often you will want to perform an effect with some context which you don't want to mutate. Const parameters are heavily used in C++ and Rust.

- Output parameters: these are parameters that are initialized by the callee and not the caller. Most languages don't need this as they can return one or more values instead, but C# is a language that has this specific feature.

4: Why invent a new term "expressive conditional" when "conditional expression" already exists? :^)


1. You’re correct, there’s no way to do that. Fortunately in this case, you’d want to use the native sort function anyway because it’ll be much faster than one written in JS/Bagel, and that is accessible in a pure context (as a pure function of an array or iterator; not in-place). There will be some barriers to certain optimizations in Bagel, but that’s done knowingly. Bagel is intended as an application-level language, so certain choices are made that prioritize maintainability over performance. With that said, performance is still a priority, and certain things like the focus on iterators over array-cloning are there to tackle common JS performance problems.

2. I’m planning to make an exception here for debugging purposes; I’m thinking a function or operator that logs a value and then evaluates to it. Not strictly pure, but close enough from a maintainability standpoint.

3. Yes, any type can be labeled “const”, which is recursive. If a proc has a const argument, an otherwise-mutable object can be passed to it and it will just be treated as const within that proc. However, a const object cannot be passed to a non-const parameter (or variable, etc), otherwise some other code could then mutate it. Assignable (output) parameters will not be supported, but contents of parameters can be mutated by procs. I specifically wanted to discourage people from using procs to generate values; they should only say what to do with the values.

4. I may not have been fully versed on the terminology :)


1. Solving that one example kind of misses the point of comment.


Oooo that's v nice. I guess yeah looping is the only thing that still feels odd. Can you even implement map in Bagel without recursion? And then there's those algos that use in/out sets in a while loop that are probably annoying to express recursively.


The assumption in Bagel is that pretty much all looping-like behaviors in a function context will be covered by iterators with chained methods; I’m borrowing (heh) heavily from Rust here (which doesn’t have pure-only contexts, but heavily encourages and supports doing things as expressions). A for-loop is a range, a for-each would just be iterating over an array, building up a single value would be a reduce, etc. There are some more gaps I still need to fill, but it should work pretty nicely for most cases.

Though of course, recursion is always still available as a fallback/preference


I wonder if a `loop`/`recur` construct like the one Clojure has would help here. Allows for closing over data without having to pass everything into a recursive function, and can handle more complex looping constructs without having to write an explicit "for loop".


Nim has actually had that distinction for a while if you're interested. As far as I remember, the "func" keyword is sugar for a "proc" annotated with some kind of flag that enables side effect analysis.


This looks really nice! I’m interested to learn more about the custom reactivity model. I am a Svelte developer, and so far, Svelte is the most economic and powerful DSL I’ve used on web. I enjoy using it every day! I would be remiss if I didn’t ask- have you tried it? I would love to know how Svelte might influence your perspective on the perfect web language, or perhaps just what your opinion of it is. I’m also fascinated by Scrimba- it looks lovely and the product that its built for (scrimba.com) is pretty incredible web tech.


Thanks!

I haven't used Svelte firsthand, but I definitely researched it as part of my process for Bagel.

As far as influence goes: Svelte definitely turned me on to the idea that you could skip a lot of the metaprogramming and overhead that MobX has to do, by injecting observe/invalidate calls at compile time. Both the bundle size and runtime memory usage should be dramatically lighter-weight as a result.

With that said, Bagel's reactivity system is different from Svelte's in a couple of key ways:

1. From what I remember, Svelte can only react to assignments to variables, not deep mutations. If you do a deep mutation, the docs instruct you to re-assign the top level object so that it triggers an update. Bagel on the other hand - like MobX - keys off of object references + property names; each pair can be subscribed to and published individually, regardless of where the object lives. Assignments to raw variables are actually a special case in Bagel; the compiler injects a hidden sentinel object to make those assignments reactive.

2. Unlike Svelte, Bagel doesn't aim to be a DSL for UIs; it's trying to be a general-purpose language. This means it can't rely on, for example, a component lifecycle for standup/teardown of reactions. This forced some rethinks, as discussed in the article, but I think it worked out in the end. The generality also means Bagel can't easily replicate Svelte's "DOM-surgery" approach to reactive UIs (which is really cool, and probably going to change the industry); so I'm sticking with a VDOM model right now. With that said, Bagel is general enough that somebody might be able to add that functionality later in the form of a library!

(I feel the need to post a disclaimer, that the current implementation of Bagel’s reactivity system is very crude end inefficient, and probably incomplete. I wanted to prove out the idea and then move on for the time being; but it will be much more fleshed out before release)

As for why I don't use Svelte personally- partly I'm uncomfortable with the lack of deep reactivity; I think it would be really easy to forget to re-assign something. The other reason is that (last I checked) it seemed like static types (via TypeScript) are somewhat of a second-class citizen. My understanding is that Svelte shipped without support for TypeScript, and it got added later as people asked for it, but it's still only "mostly" supported because of Svelte’s DSL nature and some parts don’t get fully type-checked. I'm a big advocate of static types, so this is something that bothers me.

With all that said, I have a lot of respect for the Svelte folks and I wish y'all success!


Having curried functions doesn't preclude default parameters entirely, but it requires that you signal somehow that you're not going to be giving it anything else at the call site.

A sentinel value could work, or even just passing all the args together as a tuple. Haskell and js frequently do the latter.


Still, I think requiring any sort of special calling convention across the language would stray too far from the “JS comfort zone” that I’m aiming for. Especially when it’s for the sake of a feature that was only ever a “nice to have”


Agreed, it is probably not worth it if you want to stay close to js.

Sad though, it's a great feature.


I hope Bagel has a schmear operator


I’ve been looking for a chance to do more wordplay around it :) Probably not an operator, but we’ll see what opportunities present themselves!

Edit: that would be a pretty good nickname for the “spread” operator, actually…


lox ;P


Can you elaborate on this one?





Consider applying for YC's Spring batch! Applications are open till Feb 11.

Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: