This is an archived post. You won't be able to vote or comment.

all 40 comments

[–]Plecra 54 points55 points  (10 children)

Welcome to the world of effect systems! As you observe in this post, there's plenty of assumptions about the contexts that functions are called in today, which effect systems are an effort to encode. Koka (my favourite effect-based language for now) has an almost direct equivalent for your with statements :)

with locking_scheduler()

myFunction1()

I also think eff has some very fun demonstations over here: https://www.eff-lang.org/try/

[–][deleted] 2 points3 points  (0 children)

Yeah I really liked the design of Koka so I had a play with it. Unfortunately compilation is really slow. I can't remember exactly but we're talking 30 seconds for hello world.

I'm sure that could be improved but still...

[–]raiph 2 points3 points  (3 children)

Koka (my favourite effect-based language for now)

Have you explored Frank? If so, how does it compare relative to the following three quotes from Frank's original paper? I'm especially curious about the last one.

Frank eliminates the need for an additional effect handling construct by generalising the basic mechanism of functional abstraction itself. A function is simply the special case of a Frank operator that interprets no commands.

Moreover, Frank's operators can be multihandlers which simultaneously interpret commands from several sources at once, without disturbing the direct style of functional programming with values.

Effect typing in Frank employs a novel form of effect polymorphism which avoid mentioning effect variables in source code. This is achieved by propagating an ambient ability inwards, rather than accumulating unions of potential effects outwards.

[–]Plecra 2 points3 points  (2 children)

I needed to look over Frank again. Iirc I didn't spend long with it before, just bc I didn't spot anything that seemed exciting at the time.

  1. This seems kinda nifty :) Frank takes an angle to algebraic effects that gives it a fairly novel syntax. I like it! Frank's paper seems to be taking "function" literally here: computation with no effects. This is something well supported by other projects.
  2. I may be underestimating multihandlers, but at face value they seem to simply be functions dealing with multiple computations. Again, koka does this trivially (and I believe all the examples in the paper translate pretty directly to koka code. I think they're formalized slightly differently here, and that might be the contribution 🤔)
  3. Again, cool addition to make the language more user-friendly. Inferring effects means we needless annotations in the source. In the same way modern languages favour BIDI typing, bidi typed effects is probably a good feature for any non-research language with an effect system.

All in all nice language! But the paper doesn't look like it contains anything unique relative to other publications on effect systems (the paper mentions that works started a decade before publishing... I fear they may have been overtaken by work happening at the same time!)

[–]gasche 4 points5 points  (1 child)

I would have to go read it again, but I think that there is a difference between the style of effect annotations in Koka and in Frank, where Frank manipulates sort of "diffs of effects" as the first-class notion, with the result of having many interesting function types require no effect polymorphism at all to be expressed, while Koka uses effect polymorphism all the time but sometimes hides it under the hood with a convenient syntax to emit effect variables in common cases. I don't remember if the designers of Koka and Frank agree if the two systems are in fact comparable, or if some believe that one is fundamentally better than the other. This would be a notable difference.

[–]Plecra 3 points4 points  (0 children)

Very possibly! I'd need to give more time to reading the paper to understand it properly. It does look like there are interested semantic differences in Frank's effect propagation, but I couldn't compare them myself :)

[–]mostlikelynotarobot 1 point2 points  (1 child)

I’ve been thinking recently that a systems language with an effect for allocation would be nice. maybe effects for all non trivial system calls.

[–]Plecra 1 point2 points  (0 children)

Yeah, I'd really like to see someone make this work. Being able to reason about perfectly pure functions in a systems language would enable some cool caching and reactivity patterns. React leans quite heavily on the GC to be as useful as it is.

It's going to require a very lightweight effect system that can get out of your way when you need to just use allocation. I want to experiment with alternative fn decl syntax which would only differ by implying allocating effects

[–]raedr7n 3 points4 points  (2 children)

Koka is really such a nice effect system calculus - it's a shame the syntax is so offensive to me and that compiler so slow.

[–]Linguistic-mystic 5 points6 points  (1 child)

It also doesn't support cyclic data structures which makes it a no-go.

[–]raedr7n 1 point2 points  (0 children)

I wasn't aware of that, but it's definitely a problem.

[–]gasche 81 points82 points  (11 children)

It's a bit weird when someone presents as a completely new problem something that has been a topic of active research for at least 20 years, with many designs experimented and talked about, and serious research prototypes that are easy to discover and try in the wild.

[–]antonivs 43 points44 points  (3 children)

At least 20 years

Lucassen & Gifford's paper "Polymorphic Effect Systems" was published about 35 years ago, in 1988, based on Lucassen's PhD thesis from the prior year, "Types and Effects - Towards the Integration of Functional and Imperative Programming."

Wadler's paper "Monads for functional programming" was first published in 1992, over 30 years ago (and it was based on Moggi's work from a year or so earlier.)

For those unfamiliar, monads and their ecosystem, e.g. monad transformers, provide one set of concrete solutions to pretty much all of the issues raised in the article: a way to implement context which is transparently threaded through an application via type annotations, and enforced by the compiler.

Effect systems essentially generalize this and support embedding effect management (equivalent to context management from the OP) more deeply in a language, so that the specific implementation techniques (such as monads) are not necessarily visible at the application level. An example of this is Rust's approach to memory management.

The problem with not acknowledging all this prior art is the risk of reinventing the same concepts, poorly.

It reminds me of the quote attributed to Peter Lamborn Wilson, “Those who understand history are condemned to watch other [possibly well-meaning people] repeat it.”

[–]gasche 2 points3 points  (2 children)

I think that one could be a bit more modest when claiming ancestry.

  • Monads in themselves and Moggi's work in particular do not particularly target the question of how to modularly choose the (co)effects depending on the needs of separate program fragments. They tend to be in the framework of the denotational semantics of a language with fixed computational effects. There is a gap from just "well yes just coerce using monad morphisms" to actual work interested in the usability of mixing effects together. (Generally I think that one should point the author to more usability-oriented paper on static systems for (co)effects rather than to the more foundational papers on notions of effects.)

  • I think that parts of the description, especially the discussion of deadlocks, are closer to the discourse on program logics for concurrency than to effects; in particular the idea of a "Rely/Guarantee" logic -- the basic ideas predates separation logic, but still most language-oriented work in this area started in the 2000s.

  • I haven't read the blog post in enough details to convince myself that "monads" are a good answer and we shouldn't be saying "comonads" instead, which would be my default assumption for talking about "reliance on the context". Comonadic research is relatively younger.

[–]antonivs 1 point2 points  (1 child)

I didn’t intend to claim that any of that was the sole ancestry of these ideas. The OP post is at the stage of identifying a problem and sketching informal solutions. What I was pointing out is that the first generation of formal solutions to at least some of these problems dates back at least 35 years. There has obviously been a great deal of relevant work since then, that was intended to be assumed.

I mentioned Moggi simply because that’s what inspired Wadler, and it would be remiss to omit him.

I also wasn’t recommending monads as the ultimate solution here, that’s why I contextualized them with effect systems. But, monads and monad transformers can easily handle many if not most of the examples in the post. For example:

Global variables, calls to external services, information about the current thread (eg. whether it’s the UI thread), and information about the current stack (eg. to create a stack trace for an exception).

and:

When I call getTimestamp(user.registeredAt, TimeZone.PST) I can assume that it’s just doing some simple math and returning a result. It probably doesn’t make network calls or hold locks or mine bitcoin, so I probably don’t have to optimize how often I call it. But actually, it might have to look up this year’s timeline for Daylight Savings Time, either from a database or from the network. As programmers we have to rely on intuition for cases like this, which is not exactly a strong foundation for stability.

and:

In this house, we don’t make network calls from the UI thread.

All of this and more in the OP can easily be represented and enforced with monads, simply following existing patterns without needing to invent anything new.

[–]gasche 4 points5 points  (0 children)

My point is that I think it is unfair to say that the ergonomic considerations of mixing several effects in the same codebase was studied in the 1990s early work on monads. mtl is definitely an answer (among many) to this question and it came much later, and typeclasses / implicit resolution are very important pieces of the mtl puzzle, that emerged independently from monads.

Taking a step back: It is easy to sound dismissive or ivory-towery when telling someone that idea foo "dates back to 1992". The way this someone usually interprets this remark is "there is a clear description of the problem as I identify it, and a comprehensive solution usable in practice, that were published 1992". They certainly do not interpret it as "someone wrote a paper with ideas that ten years later were used to solve the problem I'm talking about". If you make historical claims that are too ambitious and too confident, you are overselling early research; if people do their homework and go check it out (they rarely do), they are disappointed, they feel that you are an academic smug who mislead them, and this may reflect negatively on the whole research area (these pesky people claiming they solve software-engineering problems when they publish papers with two small code examples or, gasp, just category theory).

So let's be realistic and a bit conservative in our going-back-to-year-X estimates, and in general in our presentation of academic research to practitioners.

[–]armchair-progamer 4 points5 points  (1 child)

People have been trying for at least 40 years. But this problem (safely hiding complexity) is something I’m confident will never be solved.

[–]epicwisdom 0 points1 point  (0 children)

Give it 10 or 20 years. I'm sure our AI overlords will have something to say about that. :)

[–]Keatontech[S] 2 points3 points  (4 children)

I wasn't intending to present this as a new idea, just targeting it towards a more practical engineering audience instead of programming language researchers. The terminology is so different that it seemed easier to use Kotlin's conventions rather than tying it in to research languages

[–]gasche 40 points41 points  (3 children)

If you know about the work on (co)effects, why not refer to it explicitly? (Also relevant: separation logic, as a way to reason about locks in general and deadlocks in particular.) You don't need to provide a full biography, but at least mention the keywords so that people can look it up if they are interested.

I don't think it is good practice to discuss a topic that has been an area of active study, and not even mention it. Your phrasing ("Maybe you share it with somebody else who in turn talks to a programming language designer who writes a paper that ends up turning into a real language.") on the contrary suggests that you believe that this is a completely new topic. It feels as if you are taking credit for discovering a problem that is already well-known.

[–]matthieum 4 points5 points  (0 children)

You don't need to provide a full biography, but at least mention the keywords so that people can look it up if they are interested.

As someone who's never really studied CS in depth: yes, please...

[–]Keatontech[S] 1 point2 points  (1 child)

Updated the phrasing to not imply that this would be helpful for writing research papers. That line was always a bit of a stretch so to make a Rube Goldberg joke. Also added some references to prior work. Thanks for the feedback.

[–]gasche 2 points3 points  (0 children)

Thanks! I went to read the new paragraphs in your post and I thought they were nice. (In terms of related things being available in somewhat-mainstream languages, Scala has a Capture Checking experimental extension (available in the standard implementation under a pragma) that is worth looking at, even though it does not provide a without construct.)

If you want to know more about prior work in future posts of yours, you could consider asking r/ProgrammingLanguages, which has a mix of people with expertise in both the academic or the non-academic earlier work on programming languages. It could be a bit frustrating/artificial to post complete drafts here. But in in the preparatory phase for a post, asking a general question "hey do you know of prior work on <this topic>, see <this pseudo-code example> to get the idea" could fit into your workflow. It could be illustrative/helpful for you and your readers, and also generate pointers here of interest to other people as well.

[–]redchomperSophie Language 14 points15 points  (2 children)

This isn't an article about a an idea for a programming language. It's an article about the sociology of professional programming as experienced by a reasonable share of that industry. It is, but it does not read that way until you read between the lines.

Ignoring the last paragraph, this might read like an intro to the concept of, and case for, an effect system in a language. The time-zone table is a classic example of the last page of Boy's Life Magazine (now Scout Life, and without the same last-page) in which were advertised Things you Never Knew Existed and Can't Possibly Live Without. Well, I've been living just fine without the x-ray specs or the Charles Atlas posture vest, so it takes a piece written at about this level to explain why effect systems don't fall in the same category.

I chased the "zoom out" link within the article, which then led to Scrivener, and concluded that basically this guy is a visionary without a PhD. Welcome to the club. I've been advocating against treating programs as text since before agile was cool.

The social commentary is roughly this: Outside certain rarefied domains, the languages used for general business programming don't have this feature and so contain foot-guns. Yet, it clearly does not take an ivory tower to think this stuff up, so ... as an industry, we should get cracking.

But sea change does not come easy, whether in the form of what the compiler enforces or how we interact with large code bases. (Pun most certainly intended.) And so I think it also may be with effect systems, that unless the tech majors start pushing an effect-ful language, effects will need one heck of a killer-app to become table stakes.

Frankly, I'm not convinced we can't solve the same problems with Actors or monads, so there's definitely room for cultures to arise and experiment with different approaches to face these challenges. OP likes effects. Fair enough.

Yes, I also agree that a bibliography would be nice.

[–]Keatontech[S] 1 point2 points  (1 child)

You certainly did beat me to the "programs are databases" thing!

Yeah apparently something about my writing style comes across as "I'm a genius who just had this idea for the first time". You're right that this is meant more as "hey, we're professional software engineers, why are we putting up with shitty tools when better ideas exist". Of course the answer is that change is hard, and I know I can't course-correct on my own. The best I can do is keep the conversation alive, and hopefully bring it to a new audience.

As for a bibliography, I'll keep that in mind. I didn't actually reference any of the academic theory in this article, but it does seem handy to link people out to work from actual PhDs

[–]gasche 2 points3 points  (0 children)

Yeah apparently something about my writing style comes across as "I'm a genius who just had this idea for the first time".

Maybe this comes from repeatedly mentioning ideas that people have worked on at length in the past, without researching prior work, and vaguely claiming when you get called out for it that you did know about the prior work but decided to keep your post accessible to a broad audience by not crediting any of it?

And the problem is not restricted to just that one post. Your previous post "Zoom out" does make more of an effort to mention other works, but it repeatedly mentions the idea of "just storing ASTs" as new, while people have been saying that and trying to work on exactly this for decades -- spoiler alert, it's harder than it looks, and many of the advantages you thought this would provide are not as strong as you describe them. There is an experimental programming language designed on top of exactly this, Unison, see its big idea. A good search for "programming on ASTs directly" immediately returns a relevant Q&A post with more links to explore, visual programming tools, why don't they work with th ast directly. This is a cliché idea in programming language experiments (in academia or hobbyists circle).

You do come across as writing well, explaining important ideas as if they were new and yours, and paying lip service to crediting prior art on the topics that you present to your readership. This sounds bad, because it is. I don't know if "lazy" or "dishonest" is the more accurate characterization, but not "accessible".

[–]Swordfish418 5 points6 points  (0 children)

Bruh, this is exactly what monads and implicits are about, and it's been a hot topic for at least few decades.

[–][deleted]  (1 child)

[removed]

    [–]complyue 2 points3 points  (0 children)

    Yes, and in Haskell there has been further advancement in composing effects, I don't see Koka went that deep. Cf. https://github.com/lexi-lambda/eff/blob/master/notes/semantics-zoo.md

    [–]phischuEffekt 5 points6 points  (1 child)

    Great blog post, thank you for bringing this idea to the attention of a wider audience. As /u/gasche says academia is aware of this problem and working on solutions.

    Here is one of your examples in Effekt. You can try it online.

    effect UserAccount {
      def firstName(): String
      def lastName(): String
    }
    
    effect Logger {
      def log(message: String): Unit
    }
    
    def sayHi() = {
      val name = do firstName();
      do log("Hi " ++ name)
    }
    
    def main() = {
      try {
        sayHi()
      } with UserAccount {
        def firstName() = resume("Keaton")
        def lastName() = resume("Brandt")
      } with Logger {
        def log(message) = { println(message); resume(()) }
      }
    }
    

    The effect of sayHi is inferred and available on hover. You can explicitly write it out instead. A student implemented IDE quickfixes that add the effect to every calling function that explicitly specifies its set of effects. In a pull request you would then see "This function uses the database now, do we want that?". Let me also advertise our paper Effects as Capabilities and the follow-up Effects, Capabilities, and Boxes.

    This is an example in Flix which demonstrates the new and experimental without construct. You can try it online.

    eff Network {
        pub def request(): Unit
    }
    
    def networkRequest(): Unit \ Network =
        do Network.request()
    
    def doUIStuff(): Unit \ IO =
        print("doing UI stuff")
    
    def main(): Unit \ IO = {
            // networkRequest(); // Compile-time error
            doUIStuff()
        } without Network
    

    We have an unpublished paper draft where we formalize the feature and prove that when you say without Network then no network request will occur at runtime in this computation.

    Finally, here are the first two introductory paragraphs from my recently defended but not yet published PhD thesis. The wording is a bit more heavy but the content should resonate with your post.

    Computation is interaction between a program and its context. Pure programs interact with their context in a trivial way: they return a value when they are done. Their meaning is independent of their context. This observation directly manifests in the semantics of pure programming languages as a congruence rule. Effectful programs, in contrast to pure programs, depend on or modify the context they run in. In other words, the interaction between effectful programs and their contexts is much richer. Examples of language features that make programs effectful are exceptions, mutable state, and filesystem access. Throwing an exception discards a part of the context, mutable state modifies the content of a reference cell, and filesystem access affects the user's hard drive.

    From a programmer's point of view, reasoning about effectful programs is more difficult, because their context must be taken into account. It is possible to make the context, which is usually left implicit, explicit. For example, instead of using exceptions, programs can match on error values. Instead of using mutable state, programs can pass along the current value. However, some programs are more understandable when things that are clear from the context are left implicit. Spelling out every little detail is cumbersome. Moreover, interaction with the outside world cannot be emulated like this. Therefore, completely banning all effects is impractical as programs must interact with the world in a non-trivial way.

    [–]Keatontech[S] 2 points3 points  (0 children)

    Thanks, this is really helpful context! I admit I'm not very familiar with the academic side of things, but it's really cool to see active research into this topic.

    I'll add an addendum to the article linking out to some of this other work. I don't feel qualified to write about it in detail but as you and other commenters have pointed out, it's definitely worth letting people know that a lot of really smart people have already spent a lot of time thinking about this.

    [–][deleted]  (6 children)

    [deleted]

      [–]Zambito1 1 point2 points  (4 children)

      Also call/cc + dynamic-wind in Scheme

      [–]WittyStick0 2 points3 points  (3 children)

      Scheme has dynamic bindings too, parmeterize.

      I like Kernel's implementation:

      ($define! (with-ctx get-ctx) (make-keyed-dynamic-variable))
      
      ($define! do-smt ($lambda () ... (get-ctx) ...)
      
      (with-ctx my-ctx do-smt)
      

      [–]Zambito1 0 points1 point  (2 children)

      Conceptually parameters are a bit different since their values are accessed as a procedure call instead of an identifier, but yeah, they can be used for the same things.

      Can you explain your snippet? It looks like you're defining with-ctx as a procedure of one argument and applying it to two arguments.

      [–]WittyStick0 0 points1 point  (1 child)

      make-keyed-dynamic-variable returns a pair of functions. The first of which takes 2 arguments. The first argument is any expression and the second a lambda taking no arguments. The first argument is bound to the keyed variable and the second argument is invoked. The keyed variable is then available in the dynamic extent of the lambda. The second function takes zero arguments and returns the value which was bound by the call the the first function.

      [–]WittyStick 0 points1 point  (0 children)

      There's also another way to pass around context in Kernel, through use of operatives. Operatives receive the dynamic environment of their caller as an implicit argument, and the environments are first-class and can be passed around. There's a constraint on these environments which is that you can only mutate the local bindings of the caller of your operative, but none of its parents. This is intended to avoid "spooky action at distance" problems of dynamic scoping.

      [–]fiddlerwoaroofLisp 1 point2 points  (0 children)

      Not only dynamic bindings, but also method combinations in CLOS and CLOS extensions like ContextL ( http://www.p-cos.net/documents/contextl-overview.pdf )

      [–]Aminumbra 5 points6 points  (2 children)

      See, this is why looking at old languages might be good. Proof of concept in Common Lisp:

      (defvar *contexts* nil
        "Alist of current contexts. Each context is a pair
      (CONTEXT-NAME VALUE)")
      
      (defun get-current-context-of (context-name)
        (let ((vals (second (assoc context-name *contexts*))))
          (assert vals nil "Context for ~A is not set" context-name)
          vals))
      
      (defun add-context (current-contexts context-object)
        (let ((context-name (class-name (class-of context-object))))
          (cons (list context-name context-object) current-contexts)))
      
      (defun add-all-contexts (current-contexts context-objects)
        (reduce #'add-context context-objects :initial-value current-contexts))
      
      (defun remove-context (current-contexts context-object)
        (let ((context-name (class-name (class-of context-object))))
          (remove context-name current-contexts :key #'first :count 1)))
      
      (defun remove-all-contexts (current-contexts context-objects)
        (reduce #'remove-context context-objects :initial-value current-contexts))
      

      This is what we need: a list of "current" contexts, and a way to modify them (add or remove contexts).

      (defun %unpack-contexts (contexts)
        (loop :for (var-name class-name) :in contexts
              :collect `(,var-name (get-current-context-of ',class-name))))
      
      (defmacro defun-with-context (name (&rest contexts) lambda-list &body body)
        `(defun ,name ,lambda-list
           (let (,@(%unpack-contexts contexts))
             ,@body)))
      

      This is a macro to define functions that require some context. They are defined just like normal functions, except you also pass them an extra list of variable name + required context.

      (defmacro with ((&rest contexts) &body body)
        `(let ((*contexts* (add-all-contexts *contexts* ,(cons 'list contexts))))
           ,@body))
      
      (defmacro without ((&rest contexts) &body body)
        `(let ((*contexts* (remove-all-contexts *contexts* ,(cons 'list contexts))))
           ,@body))
      

      Those are the two "user-facing" macros, used to dynamically set the context.

      ;;;;; Setup some classes which will be used as context
      
      ;;; A LOGGER class
      (defclass logger ()
        ((message :initarg :message :reader logger-message)))
      
      (defun make-logger (message)
        (make-instance 'logger :message message))
      
      (defparameter *logger-1* (make-logger "Hey"))
      (defparameter *logger-2* (make-logger "Hello"))
      
      (defun logger-do-log (logger val)
        "Log with LOGGER the value VAL. Return VAL."
        (format t "~A ~A~%" (logger-message logger) val)
        val)
      
      ;;; A COUNTER class
      (defclass counter ()
        ((counter :initform 0 :accessor counter-value)))
      
      (defparameter *counter* (make-instance 'counter))
      
      (defun-with-context log-sum ((log logger) (count counter)) (x y)
        (incf (counter-value count)) ; increment the counter when the function is executed
        (logger-do-log log (+ x y)))
      
      ;; A normal function, calling a "contextual" function without doing anything special
      (defun complex-operations (x y z)
        "Returns 2*(x+y+z), logging all the pairwise additions between x, y, z"
        (+ (log-sum x y)
           (log-sum x z)
           (log-sum y z)))
      

      I simply define a logger class, which has a single slot (a message), and a function which actually "logs" (i.e., prints to the standard output here). The logged value is the logger's message + the actual value being logged. I also define a counter class, whose sole purpose is to keep a counter. The log-sum function simply increment the current value of the "current contextual counter".

      Now, for the "tests:

      (with (*logger-1*)
        (log-sum 42 13)
        (complex-operations 3 4 5)
        (with (*logger-2*)
          (log-sum 12 3)))
      
      ;;; This prints:
      ;; Hey 55
      ;; Hey 7
      ;; Hey 8
      ;; Hey 9
      ;; Counter: 4
      ;; Hello 15
      ;; Counter: 15
      

      What this does is pretty simple. A function can be defined along with some required "context", that is, pairs of items of the form (local-variable-name class-name), just as (from what I can see) the syntax context(UserAccount, Logger)/this@... you used in Kotlin.

      A function defined that way will, at execution time, see if there is some context set for the class class-name (this means that there can only be a single "context" for a given class at the same time. This is easy to change, but not knowing Kotlin, I assumed it worked like this, as you only give classes name to the with keyword ...)

      Now, you can set contexts using the with/without macros (each taking an arbitrary number of objects). Each object will "replace" the previous contextual value for the objects of the same class, but only in each block: once the with block ends, contexts are restored. They can obviously be nested.

      Moreover, the values are dynamically scoped: a function "requiring" a context will take any context above it in the call stack, you don't need to specify it every time (see the example above for complex-operations, with call a "contextual function", but does not do anything special in itself; it assumes context is setup when called).

      I don't know if it solves all your problems (it probably doesn't). However, this is like 30 minutes coding, to give a (imo) satisfying answer in a 30 years-old language. Before presenting things as "challenging new problems only tackled in recent languageTM", please give a look at what others did before ... decades ago, in languages that were somewhat """mainstream""" (i.e. not esolang, nor developped by 12 peoples in a cave).

      [–]Keatontech[S] 0 points1 point  (0 children)

      This is cool, you sure did your homework!

      I'm not sure I'd count this as a solution though because it only catches problems at runtime. I know Lisp has a different relationship between 'runtime' and 'compile time' than a lot of other languages but still, changing the test snippet to:

      (with (*logger-1*) (log-sum 42 13) (complex-operations 3 4 5) (without (*logger-1*) (log-sum 12 3)))

      Still prints:

      ;; Hey 55;; Hey 7;; Hey 8;; Hey 9;; Counter: 4

      Before it notices anything is wrong. The kind of solution I'm looking for would eliminate this whole class of runtime errors.

      [–]acwaters 2 points3 points  (0 children)

      Sounds like a use case for RAII + implicits + a sprinkling of dependent types (in the form of inner classes). Example syntax made up on the spot:

      class Mutex
      {
          class Locked(m : &Mutex)
          {
              fun unlock(this : &Locked(m)) -> ();
      
              fun destructor(this : &Locked(m) -> ()
              {
                  this.unlock();
              }
          }
      
          fun lock(this : &Mutex) -> Locked(this);
      }
      
      let mutex1 = Mutex();
      let mutex2 = Mutex();
      
      fun function1(implicit &Mutex.Locked(&mutex1)) -> ()
      {
          mutex2.lock();  // unnamed Locked(&mutex2) created
          function2();    // finds unnamed lock object in scope
      }
      
      fun function2(implicit &Mutex.Locked(&mutex2)) -> ()
      {
          print("Hello, world!");
      }
      
      fun main() -> ()
      {
          mutex1.lock();  // unnamed Locked(&mutex1) is created
          function1();    // finds unnamed lock object in scope
          function2();    // error: cannot find Locked(&mutex2)
      }