54 comments

[ 2.8 ms ] story [ 132 ms ] thread
Pretty cool work, and just as cool of a background story. Being from the bagel capital (NYC), a bit jealous you have a bagel making girlfriend. ;)

Hope to see great progress in the future!

https://smittenkitchen.com/2007/09/bronx-worthy-bagels/

This bagel recipe is the best I've tried. Not that hard to do, but it will overheat your mixer so you have to knead by hand.

+1, I've smoked my KitchenAid multiple times making this recipe but the results are well worth it
I'll have to try it out over a cool weekend. Thanks!

Needless to say it'd be me making them, and not a bagel making GF, so... the jealousy still stands ;)

This is awesome Brandon! Really like the design and feature set.

Looks like you intend to have seamless support for existing vanilla modules. Which is a very worthwhile goal IMO, and something I really like about Typescript compared to ReScript/Fable/Elm.

Have you considered how to enforce the func/proc seperation when 3rd party modules are involved? Maybe have them only callable from procs as a start?

Good luck!

Thanks!

Calling out to JS modules is still a bit of an open question. I'm actually leaning towards not allowing that (there's an escape-hatch for the internal library, but I was thinking about keeping that internal), because as you say it can throw a wrench into the guarantees made by the semantics. On the other hand, Rust has unsafe { } and its assurances remain useful even though they can technically be broken. So we'll see.

I think a much more tractable goal is allowing third-party JS to import and call Bagel code (vs the other way around). That way Bagel can still make its guarantees for its own section of the universe. Though I guess that could still open you up to problems if you, say, passed an impure lambda to a pure function which then called it.

Anyway right now I'm still waffling on these particular questions, though I also have so much more work left to do that I won't be forced to make a decision on this for quite some time :)

FWIW, one of the primary complaints of heard about Elm is that there are no escape hatches, so if it doesn't support your use case you just straight up can't use it. I've seen several reports of people avoiding it for future projects on that basis.

In contrasts, TypeScripts success seems to be in no small part due to the fact that it's trivial to drop down to the full flexibility of JavaScript as and when needed, and that it has the full JS ecosystem available to it.

Yeah, there's a definite trade-off (or really, a spectrum of possible trade-offs), which is why I'm still mulling it over
> one of the primary complaints of heard about Elm is that there are no escape hatches, so if it doesn't support your use case you just straight up can't use it.

Elm has JavaScript interop.

Here's the section of the guide that covers how to use it:

https://guide.elm-lang.org/interop/

It technically has it, but I think it's fair to say it's very unergonomic and only useful for dire needs
If a func takes a TypedArray, how will the language know which methods can be called on it? Won't you have to type all of the Javascript DOM libraries? You'd have to separate the mutating ones.
Yes, typing the standard libraries is going to be a significant task, but the author of Elm did it so I figure it should be feasible. Like Elm, I plan on exposing an opinionated subset of the native APIs, and for example I may exclude the mutating DOM APIs in favor of declarative ones. This is territory that's still up in the air though; I've got a ways to go before I really have to pin this down.
Edit: At first by TypedArray I thought you meant Array<...>; I forgot there was an actual TypedArray in JavaScript.

The answer is roughly the same though: I'll be giving Bagel types to any built-in JS APIs that get exposed, and not all of them will be included (though the list could grow over time). I'm not sure about TypedArray specifically. Generally on this sort of thing I'm looking to Elm for guidance: it's about the same level of application-focused, batteries-included, curated language that I'm aiming for. I don't believe TypedArrays are available in Elm right now (though from a quick search it looks like there's been a little bit of interest here or there).

One thing that will be interesting (thinking out loud here) is exposing certain browser APIs where both the side-effect and the return value are important, since that can't really be expressed in Bagel semantics as-is. I don't think there are a huge number of those, but it will be fun to tackle those cases.

"Generic type aliases may be forbidden to prevent type-astronauting; thanking Go for proving that a language can succeed without generics"

What are generic type aliases? Is it just any generic type, e.g. Iterator in Java?

What is the type of the compose function '|>' if it is not generic?

A "generic type alias" in TypeScript is something like:

  type FooOf<T> = Array<T> | { prop: T }
These are much more powerful than generics in languages like Java because they let you build an arbitrarily-tall tower of type abstractions. What I've found in practice is that at least when it comes to application code, these are rarely needed and cause more obfuscation than benefit.

On the other hand, generic functions in TypeScript allow you to bind together the types of arguments and/or returned values:

  function last<T>(arr: Array<T>): T {
    return arr[arr.length - 1]
  }
which I see as pretty essential. So my thinking is to allow the latter (as well as generic classes), but not the former (non-generic type aliases will still be available). There will also be some built-in generic types like promises (or some equivalent) and iterators.

|> has special language-level status and works sort of like generics, but doesn't need to be implemented in terms of them (though it may end up that way just to simplify the compiler)

> What I've found in practice is that at least when it comes to application code, these are rarely needed and cause more obfuscation than benefit

In application code, maybe; it seems to be more the kind of type-system feature that is useful in enabling more powerful libraries of abstractions than doing much for applications, as such, given the assumption of equivalent available libraries.

OTOH, while language expressive power isn't the only factor in what libraries are written in and for it, it is a factor.

I do think the feature has more legitimate uses in library code, yeah. However:

1) I'm working in a TypeScript codebase at work right now where people are often struggling with the types, because one of our main libraries got way too complex with their generic aliases. It creates error messages that are impossible to follow, and it's become a real drag on productivity, even in a library. This is in contrast with other TypeScript projects I've worked on that used different libraries and did not face this problem.

2) Bagel is being intended as an application-focused language (not unlike Go in that way). Not that people couldn't or wouldn't write libraries in it, but the expectation is that the most fundamental and general constructs will come batteries-included.

Expressivity is a double-edged sword. I'm making a particular choice about how best to strike that balance for Bagel's intended use-cases, and I'm going to see how it pans out. It also isn't set in stone yet; I plan on doing some projects in Bagel before all is said and done, as a way of seeing how this stuff impacts projects in practice.

Here is a very useful generic type alias:

    type Maybe<T> = Some<T> | Error
Not sure why you'd want to disallow that.
Right, but I plan on including really fundamental stuff like this at the language level (I don't know for sure that it will look exactly like this, but there will be a story for this situation built-in)
You don't mention error handling at all. Will it have exceptions? Can I catch exceptions in functions, or will functions be limited to calling APIs that cannot fail? So no integer division in functions? Or will it just have floats everywhere like JavaScript had?

What is the rationale for disallowing logging in pure functions? It sounds like one would be forced to develop an algorithm as a procedure, and then remove all logging and convert it into a function when its done. Then, when you want to refactor it, you'll have to convert it back to being a procedure.

That's a great point about error handling; it's something I haven't given much thought to yet for Bagel specifically

However, I will say that I generally dislike exceptions and try/catch, and I think JavaScript's implementation of it is possibly the worst one out there, so I'm definitely going to deviate from that here

Pure code should generally be free of errors, except for a handful of special cases like network requests which will almost certainly be handled as values. This will require some kind of story for nominals/enums/discriminated unions, which I'd planned to work out anyway

Things are more up in the air for procedures. Errors could become the only thing procedures "return", or I might do a(n improved) version of JavaScript's try/catch, or I might just do "nuclear" panics like Rust with no ability to catch. Gonna have to do some experimenting here to see how it fits into Bagel's split paradigm

Re: numbers, all the core data types are going to have the exact same semantics as those in JavaScript, which includes numbers

Logging won't be disallowed in pure functions; I hadn't mentioned it but I plan to include a log() pure function that does nothing but log a value and return it, so you can insert it into any expression for debugging. Technically this breaks with the "no side-effects" rule, but I think it's an acceptable compromise since the effect would never have any chance of coming back around and impacting your code's behavior later on. It's fully external.

Another thing that will help with writing pure code is "let expressions" (I'll probably use the const keyword for consistency), which allow you to set aside intermediate values during a pure expression

(comment deleted)
Maybe scala Try type could be a good thing to look at.
It looks similar to Rust's Result<> type, which is my current model for what errors-as-values might look like
> Technically this breaks with the "no side-effects" rule, but I think it's an acceptable compromise since the effect would never have any chance of coming back around and impacting your code's behavior later on. It's fully external.

Something to consider: if people come to rely on the logging in production (not just for debugging), and then you later do performance optimizations like memoization which skip calling a pure function because you know what answer it will give, then it'll break people's production code.

You can discourage it in the docs of course, but Hyrum's Law (plus my own experience with seeing how the equivalent Debug.log has been used in Elm) makes it safe to assume it'll happen!

I should look into Elm's approach; tbh I've been looking to Elm for battle-tested answers to lots of these questions, even though I'm also doing several things very differently from Elm

With that said- I'm seeing this as purely a `console.log` feature, not a generic logging feature that could go to some arbitrary logging service. And become a part of a company's infrastructure. I guess in a server context even console logging could be abused that way... but at a certain point there's only so much you can do to protect the user from themselves :)

Error handling is definitely something to think hard about. Personally, I like Maybe/Either types, especially combined with syntax to make them easier to work with, whether that's Haskell's do syntax or Rust's ? operator.
Side effects are extremely useful for debugging and telemetry, so I really don't see this being useful in any practical sense. Seems interesting in theory. Good luck!
Respectfully, there are lots of purely-functional languages out there with no side-effects at all and I'm not sure the entire lot can be written off as "[not] being useful in any practical sense".
Did you write the parser in Ragel?
I'm assuming this is just wordplay :)

But in case it isn't: the compiler is hand-written from scratch in TypeScript. I might port it to Rust one day if performance really became a problem, but for right now - especially in this exploratory stage - I'm very glad to have the flexibility that TypeScript provides.

The proc/func dichotomy is a really interesting idea! I'll be interested to see how that plays out. How do you see those fitting in as class/object members? Presumably both types would have to be able to access instance fields. Any other restrictions that crop up?

Or is the "possibly nominal types" bullet an indication that you don't plan to have things that can have members at all?

I plan on having classes, and I plan for classes to support both functions and procedures as members. If you think about it, classes already have "pure functions of themselves" in the form of getters! JS/TS has just never been able to enforce that purity before, which meant that you could technically access a property and have a side-effect:

  class Foo {
    get prop1() {
      this.state = Math.random();
      return this.state;
    }
  }

  console.log(myFoo.prop1)
  console.log(myFoo.prop1) // different!
So I think the func/proc distinction will line up really nicely with classes as containers of state. And, as in other cases, pure functions will be able to access properties and call pure functions on class instances, just not call their procedures

Other things I'm going to try out with classes are getting rid of inheritance, and adding a publicly-readable-but-privately-writable access modifier for properties

One of the fun possibilities is that funcs could be safely turned into a serialized version for sending over the wire etc.

Will bagel have a json equivalent?

Bagel values will fundamentally be JavaScript values, so the same subset of them will naturally translate to JSON

With that said I hope to give stronger typing to what's allowed to be converted to JSON, since JavaScript's serialization will happily eat up class instances (losing class information), dates (silently converting them to strings), undefined, etc.

Edit: Just noticed you said funcs and not func results

This isn't really true, since they'll still capture their closures and will definitely be able to reference other constructs like classes, etc

Changing mutable state you've closed over is a side effect, so from your description I assumed that wouldn't be allowed in a function.

Or are you talking about reading mutable state you've closed over where you're still a pure function (just with hidden arguments) but the environment might change and you can't send that over the wire.

That's what I mean. Functions can't mutate anything, period- the assignment operator won't even parse, and calls to procedures are forbidden. But they can still close over state that is mutable one other contexts

The full implications of allowing this in closures have yet to be explored and I still need to make sure they won't break the guarantees I'm wanting to offer

Very interesting project! My first thought was, now here someone has taken function coloring and made it a central language feature. But I will be really interested to see what comes out of programming with these kinds of constraints.
Hehe, yes, that occurred to me. But I've thought through a lot of kinds of scenarios and it seems like this "coloring" will lend some wonderful clarity while still accommodating all the things people might want to make with it. We'll see.

I'm really excited to get to a point where I can start doing test projects with it :)

> No null/undefined nonsense; nil replaces both

Fuck nil, maybe is much better

To be clear, there will be strong nil-checks just like there are in TypeScript and other modern languages. Commonly nil will be used in a union type:

  func foo(): number|nil => ...

  const x = foo()
  const y = x + 2 // compile error
  const z = if (x != nil) { x + 2 } else { nil }
At that point the difference compared with maybe is mostly superficial
There's two major differences between that and a proper Maybe type. First, Maybe is composable; you can have Maybe (Maybe a), which distinguishes between Nothing, Just Nothing, and Just (Just a). There's not really a way to represent that with raw nil. Second, Maybe can integrate with the rest of the type system (thinking of the Functor/Applicative/Monad hierarchy from Haskell), which lets you easily have code like

  func foo(): Maybe<number> => ...
  x = foo()
  y = fmap someOtherFunction x
instead of the more verbose y = if (x == nil) { nil } else { someOtherFunction x }
I agree the if/else version here is a little verbose; I'll probably try to make this syntactically cleaner in some way. Maybe a Python-style `X if condition else Y`, maybe the parens and/or curlies could be removed, maybe this is a case for the upcoming expressive-switch or match syntax. There's a small chance I might even give some kind of dedicated map-like construct for |nils, since that will be such a common case.

However, I want to avoid getting too deep into FP-land. The hope is for Bagel to be familiar and comfortable to existing JavaScript devs, where they can benefit from a degree of purity while being able to understand or at least guess at the meaning of a given construct in front of them based on their prior experience. To that end, I think terms like "either" will be unfamiliar, and I also think situations like (Just (Just X)) will create a lot more confusion than utility. I'm struggling to think of a useful case for that myself (I'm sure they exist, but the point remains).

I'm thinking about the teams I've worked with, and I'm thinking about what would most effectively empower them to benefit from this "stateless subset" paradigm. A guiding principle is that I want Bagel to be a language that introduces some idealism into a pragmatic context, not one that's idealistic to the detriment of usability

Addendum: I gave this some more thought and realized that actually, even as I have the compiler implemented right now, the else clause above is redundant because an expressive if/else with no else defaults to nil for that case:

  const z = if (x != nil) { x + 2 } // comes out to number|nil
So that's less verbose and may be acceptable as-is. You also shouldn't need to chain the syntax for multiple values:

  const z = if (x != nil && y != nil && z != nil) {
    x + y + z
  }
There may also be room to make the () optional like Rust does, or maybe just dropping the {} would be better. I'll have to see how each affects parsing and readability
Definitely a few interesting ideas here. As someone who dabbles in Haskell, I'm certainly all in favor of separating pure code from side-effecting code.

I'm not quite sure how the constraint that procedures can't return values would work with fetching data from external sources; I suppose that ties in with the reactivity bit? That could work for long-running applications, such as the web apps you seem to mostly have in mind, but for one-off scripts or anything operating in more of a batch mode it seems overly complicated.

Building off of that point, it might be better to focus more narrowly on web apps as the target domain, rather than trying to make a general-purpose language. Maybe don't focus the language as tightly as Elm does (where SPAs are the only thing it really supports), but make clear that web apps (and more broadly, reactive GUI applications) come first.

Deep const is an interesting idea that I think is worth exploring. While I partially like Haskell's approach of making everything immutable (besides stuff like the ST monad), there's definitely some logic that's easier to express in a mutable fashion. Might be worth looking at the immer library for presenting a mutable interface for working with immutable objects; maybe with a language construct that allows mutability only within a certain scope?

Fetching data will definitely be one of the more interesting use cases for the func/proc separation. However, I've got a pattern I've already had success with in MobX which should translate here:

- loading state, response value, and any error are all observable state (wrapped up in a class)

- at creation time you pass a lambda that just generates a promise; this function is set to automatically re-run whenever its dependencies change (this will need to be slightly different for Bagel because promises run eagerly; I plan on somehow expressing a "lazy request" or "request spec" as a pure value that can be generated in a pure context and then requested later in a procedural context)

I've seen Immer, it's a very cool idea. Right now I'm planning to stick with raw values even for immutable code, though I'm definitely interested in optimizing over that if I can do so transparently

> maybe with a language construct that allows mutability only within a certain scope?

I've already got a language construct that allows mutability only within a certain scope ;)

As for priorities/focus, I would say the order is:

1) Web GUIs

2) Web Servers

3) Games (probably web-based)

4) Everything else (scripts, etc)

I have interest in supporting all of these, and I don't think anything I'm doing will be prohibitive of any of them, but when faced with a choice that's good for one at the expense of another, higher priorities win

Though I will say:

> but for one-off scripts or anything operating in more of a batch mode it seems overly complicated

I disagree; I think I can make the reactive model ergonomic enough that it wouldn't be poorly-suited even to casual scripting. It may not benefit you as much there, but I don't think it will hinder you.

For me the killer feature of anything making the pure and stateful divide explicit would be the automatic parallelization independent pure expressions.

A parallel iterator for map (or any other pure function batch operation) allows for an entire world of possibilities.

I'm considering having some way of easily making functions concurrent, however this is limited by the fact that JavaScript doesn't have memory-sharing threads; it only has workers that can pass messages to each other. So any boundary data has to be serializable, and the benefit of processing it in parallel has to be greater than the cost of serializing and deserializing it. I've only really encountered one situation in my career where that trade-off made sense.

With that said, I'm going to probably try out something along the lines of a "concurrent" keyword that could be put on a pure function to automatically spin it out when it's called

Have you looked at Fortress at all? It was designed on just this premise, and came to some interesting conclusions before being abandoned. For example, where the natural data structure for Lisp is the list, the natural data structure for Fortress is the tree.

(Discussed here: https://news.ycombinator.com/item?id=814632)

This is very interesting, but it seems like its goals were pretty different from Bagel's. That said I always love learning about a new esoteric programming language!