1. 1

    The dynamically scoped global context made me happy. No other language except Common Lisp has that as far as I know. I am excited to use this.

    1. 2

      Won’t that make optimizations extremely hard? I haven’t watched the video, so I don’t know the details (and the Jai language primer makes no mentions of contexts), but if you can’t tell statically what’s in scope, it seems to me that most analyses will have to conservatively assume that the universe is in scope, no?

      1. 3

        Things may have changed from the last demo I saw of Jai contexts, but this seems to be something intended to be used sparingly, or at least the context should contain only a few root object pointers. Functions that use context simply desugar to context-passing-style. The really interesting problem is what to do about higher-order code.

        On other thing that makes this easier: Jai is focused on fast full-compilation, so it doesn’t suffer from the usual restrictions imposed by separate compilation. It would be possible to do conservative global analysis (very cheaply!) to compute which functions need which partitions of the whole context.

        1. 1

          Scope and optimization here are separate questions and I don’t see how they’re related. Regarding scope, I don’t know the full details but I would assume you have to declare the global variables beforehand, so it’s not like you can introduce arbitrary variables into the context. The compiler knows exactly which static addresses are accessible and which are not. Perhaps that answers your question?

      1. 5

        Currying makes sense in Haskell/ML, but with very few exceptions, it just doesn’t fit in to Clojure. The obvious argument is that Clojure, like many other languages, chooses a syntax that prefers variadics to the exclusion of currying by default.

        More importantly, currying encourages patterns that should be avoided. For example, functional combinators and monadic interfaces thread a context argument in the rightmost position. For one thing, these patterns produce unprintable/opaque closures, which deeply hinders debugging. That’s a shame in a language that ships with a pretty printer for - and encourages use of - a common, closed, syntactic data structure (EDN). For another thing, Clojure is impure, so you can just have effects, rather than introduce the extra complexity of monads which make it much easier to accidentally produce lazy or duplicated effects (since the monadic values are reified, the implicit linearity of imperative constructs is lost). Instead of combinators or monads, just write a direct-style, effectful interpreter for some EDN-encoded value, pass a context map as the first argument, if you need to.

        Never mind the fact that currying introduces lots of anonymous values, which can easily lead to the sort of confusion that you’d want a type checker to alleviate!

        1. 1

          This version of curry supports variadic arguments. Other issues you’ve mentioned may be relevant or not, but are the same issues that Clojure’s partial has.

          As for the monads and effects, monads are not only about effects, but more generally about threading context, so although they are less needed in Clojure than in Haskell, they can be useful.

          1. 3

            This version of curry supports variadic arguments.

            It only “supports” defaulting to an arity of 2 and allowing you to declare a higher fixed arity. Currying and variadic function application are fundamentally incompatible.

            the same issues that Clojure’s partial has.

            I think that partial and comp are overused in Clojure as well. For top-level declarations, the syntactic overhead of using named parameters serves as useful documentation and you get the right code-reloading behavior implicitly through var resolution. Otherwise, you have to remember to (comp #'some-named-var #'some-other-named-var) to avoid some confusion later. For inline functions, the #(....%....) shorthand syntax is still quite short and much clearer and more flexible than either comp or partial.

            monads are not only about effects, but more generally about threading context

            I think my comment covered this, but I’ll restate more concretely: You should pass “pure” context via a map as the first argument; and you should pass “impure” context as either thread-local global vars or by putting a reference type in your pure map.

            The interplay of currying and context threading is that you can define methods on initial parameters to delay computation and then provide the context later to force evaluation. In a strict, impure language, it’s much preferable to just do a thing rather than to build up an opaque structure that does a thing. If you actually need delayed and context-varying execution, it’s much better to design a transparent structure to be interpreted. Then the context can be threaded through the interpreter directly.

        1. 11

          I’m experimenting with something new for my new startup. I’m calling it “fractional” remote: Everybody is encouraged to work from home Wednesday through Friday. Open to better names for this too.

          So far, we’re all really enjoying the balance.

          • You get to connect with everybody Monday/Tuesday.
          • All meetings are scheduled for those two in office days, which is more than flexible enough to enable that kind of collaboration, which is often necessary.
          • You get a solid three days of productive time to do independent work.
          • Remote workers aren’t considered second-class, since everybody is a “remote” worker.
          • Fewer days commuting means that my hiring pool is a bit larger, as people can stomach a longer commute for two days rather than five.
          • You don’t miss out on important discussions because nobody is in the office when you aren’t.
          • All important communications are likely to recorded electronically, since remote communication days dominate the calendar.

          BTW, if you’re in (or near!) Seattle. I’m hiring :) Contact details in profile.

          1. 2

            Oh the irony - a seemingly remote-friendly approach, looking for people in a single city in a single country.

            I do hope your experiment works out though, so good luck!

            1. 2

              It should at least make the catchment a bit wider, though. There are some people who won’t commute to X every day, or most days, but who would do one or two days. I’ve worked with someone like that, who had a very long commute, and it was fine (though it for me it really underscored the question “so why do the rest of us have to be in here every day?”)

              1. 1

                Yeah, the Seattle-area commutes are amongst the worst I’ve ever seen, so weirdly this actually sounds like a god thing.

                I love living in a city with proper transport though.

              2. 2

                Right now, we’re pretty small. As the company grows, my intention is to also grow the geographical area we hire from.

                I still believe that face-to-face time is critically valuable. Having myself been a remote worker on distributed teams, I’ve seen first hand how it requires the right kind of people, culture, and experience to make it work effectively. There is a real human cost to not having regular face-to-face interactions. The baseline is remote-friendly practices, which I think we’re on track for. As the team grows, work becomes more clearly defined, and individuals become more specialized, the communication overhead of remote-work are reduced.

                If were didn’t do this, we could only really hire from Seattle proper. Maybe some folks who don’t mind a long commute too. But, with this policy, all of a sudden everybody on the east side of Lake Washington may be more willing to take a chance on such a job. An hour+ commute daily across the bridge is hellish, but doing it only twice per week, and probably only commuting around 10am and 3pm or so, and suddenly it’s not that bad. Same goes for cities neighboring to the north and south.

                The next step would be hiring from Portland, Oregon or Vancouver, Canada. Commute a bunch for your first few weeks, we’ll put you up in a hotel, and then reduce your commute schedule to monthly, and then eventually only commute for a week each quarter, or something like that. Once we can afford it, this option can be opened up to people who need to commute by plane too. Eventually, the “100%” remote is likely, but even then, I’d want to make sure people interact face-to-face at least several times per year.

              3. 2

                that kind of collaboration, which is often necessary

                Can you write more about why in-person collaboration is often necessary? I’m a fan of the remote-only option, but I’d like to understand other perspectives.

                1. 3

                  Two reasons:

                  1. Face-to-face communication has higher information bandwidth than any other form of communication.

                  My new business involves the marriage of legal and engineering expertise. On more than one occasion in the short lifespan of this organization, a week-long engineering confusion was resolved by physically looking over the shoulder of the paralegal assembling a binder of paperwork. In past remote work, I’ve experienced escalation from email, to IM, to phone call, to video conference, and at least once to “fuck it, I’m getting on a plane and coming over there”. In my opinion, an escalation process like that should be more common rather than less. If you want to be competitive, you shouldn’t completely eliminate your highest-bandwidth communication channel. Besides, there’s still no better collaboration tool than shared physical writing surfaces.

                  1. Face-to-face communication enables bonding that soothes tensions and smooths work.

                  I’ve worked on several split-office, but non-remote teams. The narrative at each was “everybody in $other_city is an idiot”, but at the end of a week long visit, “oh they’re not so bad”. I have lots of regular internet acquaintances. The only ones who have become “friends” are those I’ve run in to physically at conferences over and over again. I’ve contracted work remotely to people with or without having worked with them locally before. A month of working together locally builds the same trust as a year’s worth of remote work. As far as I can tell, these are not unique experiences.

                  1. 2

                    Thanks for sharing your perspective. Now, here’s more on mine.

                    For me, the advantages of remote-only work all come down to maximizing inclusion.

                    1. Face-to-face communication, video conferencing, and (to a lesser extent) audio convey a lot of irrelevant information that could be a distraction and even trigger unconscious biases: the person’s appearance, clothing, accent, etc. In text, a person is just their name (or possibly a pseudonym) and their words.

                    2. A remote-only team, particularly using primarily text, is more inclusive of people with disabilities – blind, deaf, mobility impaired, speech impediments, etc.

                    I’ll grant, though, that the ideal remote-only, text-only environment that I’m advocating here would probably feel very constrained for most people.

                    Also, I’ve only experienced the remote-only option in the context of a company where most of the staff were blind (or at least visually impaired like me). So maybe it only worked for us because we were used to doing without the high-bandwidth channel of vision in the first place. And even we relied heavily on voice communication. Sometime I want to try doing a team project from start to finish with nothing more than text.

                    1. 2

                      I ran a remote team for a couple of years and I have to agree with @brandonbloom. As much as I prefer text communication, it just didn’t work all that well. We used Slack, email and video calls. We regularly failed to resolve things via Slack and email, whether regarding requirements or development problems. It is very hard to explain things clearly without writing a wall of text, and nobody wants to write a wall of text (or has the time to do it). Video calls and screen sharing allowed us to have a much more productive dialog.

                      Contrariwise, I didn’t feel a particular need to escalate further and meet in person. For me, it was enough to spend a bit of time in person to get acquainted in the beginning, and after that video calls were enough both to carry the relationship forward and to resolve any issues.

                2. 1

                  I’ve thought about giving this a try, it’s cool to know someone is already doing it! A couple of questions: does this happen only to your team or is it for the whole company? What happens when someone can’t come on a be-in-office-day?

                  1. 2

                    We’re a small startup still, so it’s the whole company.

                    If you can’t come in the office for whatever reason, it’s no different than other flexible schedule companies where you’d either take time off, or just WFH that day to get your big delivery, or rearrange hours to deal with a personal matter, or whatever. The key difference is that a large portion of the non-productivity reasons to WFH are schedule-able, so you can just plan to have your couch or whatever delivered on a Thursday. But “life happens”, so if something unplanned comes up, that’s perfectly OK. Since electronic communication is the norm W-F, you hopefully won’t have missed too much.

                  2. 1

                    i’ve wished i was in this situation for a while now after experiencing both “remote only” and “no remote”.

                    Remote is great when working alone. When hunkering down and actually coding, working remote is great. Brain not currently working? No problem, I’ll just take a break for 2 hours and work tonight. Or, wake up and “in the zone”, let me hack for 12 hours today with no interruptions.

                    At the same time, when I was remote, not being able to have the occasional sync up in person definitely made work more difficult.

                    So I think your idea is great and as long as your team is honest/responsible I think it should work out well. Would be interested in knowing how it works out.

                  1. 9

                    I was expecting this to be about creating a secure string type, but forcing the lifetime to be ’static is much more clever! Fun.

                    1. 25

                      the initial request is rigid and does not include a field that could be used to request that new servers modify their response without breaking compatibility with existing servers

                      we needed to find a side channel which existing servers would ignore but could be used to safely communicate with newer servers.

                      Folks: Always put version numbers in your protocols and file formats. Oh, and while you’re add it, prefer extensible records too.

                      1. 14

                        In some IBM products I saw their smart design around C data structures and wire protocols: everything has in the first few bytes a static ascii “eyecatcher”, a version, a header offset, a payload offset, lengths, and usually a checksum. I’ve had arguments about this since but I believe the dozen byte trade off is worth it since within seconds you can identify the protocol and the payload, you know if you got the whole thing, you can detect (some) corruption, and if a future version of the structure is released the old one still works.

                        1. 7

                          This is done in their mainframe object code as well, specifically in the function prolog for XPLINK (and other linkage conventions, I believe). Every function is laid out with a header that starts with 0x00C300C500C500F1 (“CEE1” in EBCDIC, with null bytes) and some other bits of information that lets you walk the stack very easily. It also lets you get at the name of the function so not only can you walk the stack, but you can get the function name. This means you don’t need debugging information to get a stack trace. It’s also really useful is you are searching a raw hex dump.

                          1. 1

                            These were WebSphere MQ and DB2 LUW, which are strongly influenced by their mainframe heritage.

                            I didn’t know about the linkage convention but assumed they must because on occasion I’d uploaded core dumps and they trivially walked them and someone mentioned once that they had an internal tool that did it. Their trace output was also top notch, really fantastic and clearly designed in from the start.

                          2. 2

                            Is that just a thing that IBM does with all their data structures? Company policy or something?

                            1. 2

                              No idea, but I wish it were externally explained and documented somewhere.

                          3. 3

                            I can entirely agree with you on that. The time it takes to implement a version number field is negligent compared to the amount of pain you’ll need to dedicate to supporting an old format with no version field.

                          1. 1

                            Discussions of infinity hints at why it’s nonsensical to say that the axiom of choice is “right” or “wrong”. It’s either depending on context. It’s related to the closed vs open world assumption. The reality is that, in practice, you have alternating layers of closed systems and open systems. Axioms such as that of choice, or the law of the excluded middle, are useful in closed contexts and invalid in open, extensible contexts. Use the tools that are applicable to your situation.

                            1. 3

                              It’s not literally right or wrong, it’s just a matter of taste if you want literally undescribable Lovecraftian monsters in your results or not. Personally, I don’t consider them “useful”.

                            1. 5

                              This post is great! This has helped me clarify my thinking about a few things:

                              You just can’t do without being able to break your program into pieces.

                              I’ve found that “pure” code wants to be broken up in to smaller and smaller pieces. This often comes at a performance cost for both call overhead and duplicate work, but that performance is often easy to recover with bulk pure operations, at some risk of repeating yourself.

                              However, imperative code is often dramatically clearer as big linear blocks of code. More and more, I’m just inlining procedural functions at their call sites with few if any ill effects, and usually benefits. Similarly, instead of extracting procedures, I’m now frequently creating intermediate maps or parallel arrays, and then looping over them.

                              The one big reason I still break up imperative pieces is for open-dispatch. However, even that can usually be separated along functional and imperative lines: the functional code operates on open union types, but return values of a closed sum type to communicate desires to a central imperative procedure.

                              Functional code wants to be composed. Imperative code wants to be parameterized.

                              runST operator lets you create computations that use highly aliasable state

                              This is why I’m a monad detractor. Once you get in to a sufficiently complex monad, your right back where you started with imperative programming. Only now, you have the added complexity that everything is effectively “quoted” by default, so you can accidentally “metaprogram” your way in to a mess, re-ordering sub-procedures and the like. The type system won’t tell you that should have bound an intermediate result to a variable, rather than splice in a chain of first-class statements.

                              I don’t doubt that reasoning about imperative programs in Haskell is easier than in say Ruby, but I think that’s more related to the amount of aliased state by default, more than anything else.

                              Removing any one of the three difficulties makes things easier […snip…] a) State + procedure calls b) State + aliasing c) Aliasing + procedures

                              I’m designing a language that embraces immutability, but has statements and imperative constructs. Some core design elements aim directly at spending as much time in these two-out-of-three-difficulties sweet-spots as much as possible.

                              The main thing that values are deeply immutable and acyclic. They are strictly separated from variables (of various kinds) that are mutable containers of such values. Cycles must be achieved via indirecting through a symbol or ID. Stateful objects may employ internal mutability for performance, but then must perform copy-in/copy-out to preserve the value illusion.

                              A) A reduction in aliasing of mutable state across procedure calls is achieved in two ways: 1) defaulting stateful constructs to be second-class. You need to use explicit syntactic constructs to reify them as objects, like address-of (&) in C or Go; quite unlike Java or Ruby, etc. 2) state has structured lifetime. First-class procedures can close over state, but they will throw if the procedure is executed outside the dynamic extent of that state. If you want to share state more widely, you need to allocate it explicitly from higher up in the process tree.

                              B/C) Procedures interact with stateful aliasing less frequently with deeply immutable values and explicit mutable aliasing. While call-by-value is standard now, it’s less useful when you have pervasive aliasing in order to pass large values or values of dynamic size or type. In Go for example, you get much more milage from call-by-value behavior than Java, but still frequently create pointers in order to escape the overhead of excessive copying. Not a problem with deeply immutable values.

                              1. 11

                                Slightly (greatly?) less provocative title: “S-Expressions are not ASTs”.

                                With respect to specific points, Clojure and Racket have good solutions for many of these things.

                                RE #1: Annotations can be handled generically. Take a look at Clojure’s metadata facilities: https://clojure.org/reference/metadata

                                RE #2 cons: Clojure’s data structures are abstract (defined in terms of interfaces) and extensible (there is a tagged-literal type). Check out EDN: https://github.com/edn-format/edn - or more general these resources: https://clojure.org/reference/data_structures and https://clojure.org/reference/reader

                                RE #2 compiler internals: The ClojureScript compiler uses an AST made out of recursive maps. Works quite nicely. “tools.analyzer” does this too. Check out the readme: https://github.com/clojure/tools.analyzer

                                RE #2 & #3 special backquote: Racket does exactly this with “syntax objects”. Racket’s syntax objects only capture lexical origin and scope nesting information, but you can go even further with this. For example, MetaOCaml, Terra, or other staging systems can capture full lexical closures attached to first-class, user-level syntax fragments. See http://okmij.org/ftp/ML/MetaOCaml.html and http://terralang.org

                                1. 3

                                  There’s no reason you couldn’t invent some new type that has the type system rules you want. The reason functions are typically handled structurally is because it’s typically what you want and you can recover nominal typing if you need it. Just wrap a structurally typed thing in a nominally typed container. For example, instead of a function, use an interface/class with a method.

                                  1. 2

                                    Just wrap a structurally typed thing in a nominally typed container.

                                    Like how Java did it before the addition of lambdas to the language. The problem there is that there has to be a way to declare the “function type” inside the nominally typed thing. If your system has structs, you could have a function in a struct…but how do you declare the type of the member of the struct without resorting to structural typing?

                                    If you have objects/classes where the function becomes a method of the class, that’s pretty close to working. The problem there for me is that there’s no classes in the type system that I’m thinking of. I realize that’s adding constraints after the fact, so it’s not really a fair objection. :)

                                    1. 3

                                      there’s no classes in the type system that I’m thinking of

                                      Just as you can embed a structural type in to a nominal context, you can embed a nominal type in a structural context. In fact, one way of looking at things is that nominal typing is really just the combination of structural typing plus “brands”, where brands are a special case of dependent types with type generativity. In the case of classes, the type depends on the value of the class name and the act of declaring a class generates a new class ID (some fully qualified name, or some gensym, or a memory address, whatever). So rather than adding classes to recover nominal typing, you can just add a pair of type-level operations for generating a brand and for embedding a brand in to a structural type.

                                      For example:

                                      class C {
                                        method: (int) -> int;
                                      }
                                      

                                      can be:

                                      brand C;
                                      
                                      struct S {
                                        static field = constant(C);
                                        method: (int) -> int;
                                      }
                                      

                                      Your solution doesn’t need the full generality of a “constant” evaluation form, or “literal types” or anything like that. Just trying to illustrate the idea.

                                  1. 4

                                    This is a pretty good summary of the problem and solution space known so far. However, despite mentioning Julia, you didn’t mention the fact that Julia has a user extensible promotion system!

                                    It’s a little ad-hoc, since it’s built on top of arbitrary parameter combinations + a fallback case that does the promotion. So users can add rules where types don’t match. However, you could look at that community a bit to see what the fallout has been from offering this functionality.

                                    As for your unsolved problem about enforcing the lattice: That is indeed a hard problem! I saw these “verified” classes in Idris. The idea is that types as proofs are members of the class, so that you can require proofs of the lattice properties. As a dynamic typing fan myself, I find that approach a little heavy handed for most users. However, the same idea could be applied more gently. For example, you could provide a library of properties that are readily usable with a “generative testing” framework.

                                    1. 1

                                      This makes me appreciate zero values in Go. Instead of having to write a builder, I’d just declare a variable and set fields on it. I realize Rust insists on explicit initialization, but you could at least approximate that here: just write a function that returns a struct and set fields on that. What’s the advantage of the with_whatever methods?

                                      1. 14

                                        If this is your use case, Rust has a protocol around the Default trait and field initialization syntax.

                                        #[derive(Default)]
                                        struct Foo {
                                          field1: u32,
                                          field2: u32
                                        }
                                        
                                        fn main() {
                                          let foo = Foo {
                                            field1: 1,
                                            .. Foo::default()
                                          };
                                        }
                                        

                                        Alternatively, you can implement Default on that.

                                        impl Default for Foo {
                                          fn default() -> Foo {
                                            Foo { field1: 3, field2: 2 }
                                          }
                                        }
                                        

                                        The advantage of Builders is that they are lazy and can be passed around. So, for example, I can have a library that pre-builds requests in a certain fashion and then hand them off to a user defined function that sets additional data.

                                        1. 1

                                          I don’t get it. I can pass around an object and set fields on it too, what’s the advantage?

                                          1. 10

                                            Struct fields in Rust are private by default, so having it leave scope might lead to people not being allowed to assign those fields.

                                            Additionally, if forgot this: Rust is a generic language and patterns like the following aren’t uncommon.

                                            fn with_path<P: AsRef<Path>>(&mut self, pathlike: P) {
                                             ....
                                            }
                                            

                                            Which means that the method takes anything that can be turned into a (filesystem)-Path. A string, a path, an extensible pathbuffer, etc.

                                            It enables you strictly more things to do. Yes, at the cost of some verbosity, which you can avoid in simple cases.

                                            1. 5

                                              A TBuilder doesn’t type check as a valid T object. This is the real value of the builder pattern (in Rust and Go and Java and…Haskell): one can write a fairly strict definition for T, and wherever one has a function that accepts a T, one can be sure that the T is fully constructed and valid to use. The TBuilder is there for your CLI parser, web API, or chatbot to use, while stitching together a full object from defaults+input, or some other combination of sources.

                                              Distinguishing between T and TBuilder prevents a partial object from masquerading as a full object.

                                          2. 7

                                            This is an orthogonal thing for the most part. For example, sometimes I use builders in Go when the initialization logic is more complicated.

                                          1. 7

                                            At what point does the type become rich enough that it might as well just be the implementation?

                                            Or to be less facetious: When do we start seeing mainstream languages with integrated relational and constraint programming?

                                            1. 2

                                              The flaw in your facetious statement is the definite article, the. The type can be a implementation: a slow (or otherwise inefficient), but ascertained-to-be-correct implementation, while “the implementation” is a fast, less-likely-to-be-precise one.

                                              I don’t see this as connected to relational and constraint programming. These are ideas I use on a regular basis; there are other times when I use relations and constraints; and when I’m doing one I’m rarely (by choice) doing the other.

                                              Though Racket comes with Datalog built-in, if that’s really your thing. (-:

                                            1. 17

                                              Types let you represent how much information you have. If you want to deal with “arbitrary JSON which might be missing information at various points” there’s a type for that called Json and it supports automatic merges. If you want to inspect that type and learn small bits of information about it you have types for that as well, for instance

                                              isTopLevelObject :: Json -> Maybe (Map Text Json)
                                              isTopLevelArray :: Json -> Maybe [Json]
                                              isString :: Json -> Maybe String
                                              

                                              This “attempt to move up the information hierarchy to something more strict but possibly fail” is a very general pattern that usually goes by the name of “parsing”. In a program that uses types well you are constantly “parsing” your way to more information and typically should do so incrementally. If you “parse” all the way to a “concretion” then you’re asserting that your program is strict/brittle. That might be right… but if it’s not then you’ve just made a design mistake.

                                              ADTs are too heavily used. Row types are clearly a win for many sorts of systems letting types flow more freely when the time is right.

                                              That said, typed languages contain within them pretty much all gradations of untyped languages as well if you want to use those. This is the pragmatic heart of Harper’s much maligned “dynamic types are monotypes” argument.

                                              If you find (static) type information is never useful then you shouldn’t use a statically typed language. If you find it’s sometimes useful then you should use one and build types which represent your uncertainty. All of the “dynamic language tricks” will be available to you to use atop those “uncertain” types.

                                              1. 3

                                                tel: First, let me say that I mostly agree with your comments on this page, even though I’m a fan of dynamic languages over static ones.

                                                This notion of the transition from looser to stricter types as “parsing” deeply matches my intuition. However, my experience has been that most type systems are quite bad for this. TypeScript has been the first type system that I’ve used that isn’t totally unbearable for this task, because of support for associative data, optional keys, union and intersection types, subtyping, etc. This stuff is important when the results of parsing contains substantial partiality and inherit domain complexity.

                                                I’d also like to note that defaulting to any subset of static, nominal, closed, positional, etc. creates a path of least resistance that produces brittleness. You can say “you’ve just made a design mistake”, but even knowing better, I make that design mistake every time and it’s physically arduous to change my program when I need to. Furthermore, the power of a type declaration to constrain my program’s behavior great enough that it often leaves only little holes. It becomes a puzzle to start filling in those holes. That temptation is so strong that even I can’t resist it sometimes, losing hours of work, only to give up and add a cast with a dynamic guard. At least with increasing experience, I can predict that outcome earlier and earlier each time.

                                                1. 2

                                                  I would like to second that TypeScript has one of the best type systems for the sort of messiness that arises in the outer layers of programs.

                                                  It has extremely powerful tools that let your merge types, easily construct union types without having to add a boilerplate ADT layer, and has a very smart resolver for letting you then drill down to the right types without a bunch of superfluous casts. And it has a la carte exhaustiveness checks! Sometimes you have to trick the compiler since its structural typing for the most part, but you can find safe ways of doing it.

                                                  We’re still not to the dream of a type system that lets you easily jump between value and type and handling partial information, but Typescript is getting us there.

                                                  1. 1

                                                    dream of a type system that lets you easily jump between value and type and handling partial information

                                                    What is this dream?

                                                  2. 2

                                                    I think I’d love to understand your experience with Typescript more. I used it a while back a touch and bounced off (not using types to attack nulls seemed a bridge too far to my intuition, but that may have changed or improved since I last looked).

                                                    Generally, I find that a set of lenses atop of a general Json type is an extremely nice place for dealing with things like missing data and the like in a sensible and convenient way. That said, I really like what row types and open sums/products can do and these open doors to very nice structural subtyping similar to what I think you’re discussing w.r.t. Typescript.

                                                    So, yeah, can I encourage you to write about this topic through the lens of Typescript? I’d love to better see your perspective here :)

                                                    1. 1

                                                      not using types to attack nulls seemed a bridge too far to my intuition

                                                      You can union null (or undefined) in to any type like this: T | null. However, it’s often preferable to model optionality at the level of the aggregate: {foo?: T} where this essentially means {} | {foo: T}. “Occurrence typing” is how you dynamically cast something like a T | null | undefined to a T: a ? a.b : 'whoops'. This also works with statements, such as an if-block. You can also use Partial<T> to explicitly decorate every key in T with ?, then massage that Partial with a union, intersection, etc.

                                                      a set of lenses atop of a general Json type

                                                      Lenses are a whole ‘nother topic. I think they’re a great idea, but current realizations have some usability problems that I won’t get in to here. Nor will I get in to how I think lenses could be made more accessible.

                                                      can I encourage you to write about this topic through the lens of Typescript?

                                                      Do you mean use of open records and subtyping specifically?

                                                      1. 1

                                                        Yeah, I believe that explicit sort of null handling is newer? Maybe I’m mistaken, though.

                                                        I’d like to understand more completely how and why Typescript is good at handling input/boundary data well. I can intuit it from the feature set and believe it from the fact that it’s deployed in Javascript and frontends are full of these problems, but I’d like to understand it better from a practical POV since I think a lot of this comes down to UX.

                                                        Maybe it’s: “how do the constellation of type system decisions made in Typescript combine to make it really good at handling messy input data?”

                                                        1. 1

                                                          I think the answer has to do with 1) JSON is actually quite a good data representation. And 2) The TypeScript team refuses to change the semantics of JavaScript.

                                                          Now, JSON has a lot of problems, like the lack of extensibility (see tagged literals in Clojure/EDN); isn’t a great language for config files and such (no comments); no head/body syntactic-term type (like lists in EDN, or normal ADT constructors); and lots more problems. But overall, the presence of open associative structures makes it quite good for representing things and surviving growth: Just add some more keys! This is critically important for future-proofing systems. See Rich Hickey’s “Speculation” talk for more about “Growth”: https://www.youtube.com/watch?v=oyLBGkS5ICk

                                                          Second, by insisting on not changing the semantics of JavaScript, the TypeScript team was forced to build type system features that support open and extensible data. For example: With associative data, there is a neat trick that unknown keys are disallowed in literals, but are perfectly allowable in dynamic values. This means that you can survive extra data. Coupled with the reflective capabilities such as Object.keys you can write code that can safely be future proof, but to new data from clients, but also to new requirements internally.

                                                          Another thing is the inclusion of type assertions, which let you hint to the type system without being forced to reconstruct a type completely. This means that if you get JSON on the wire, you can write a predicate that asserts new static information about it:

                                                          let isStrings = (x: any): x is string[] => {
                                                            return Array.isArray(x) && x.every(y => typeof y === 'string');
                                                          };
                                                          

                                                          Now you can do something like:

                                                          if (!isString(x)) {
                                                            throw Error('expected strings!');
                                                          }
                                                          x.forEach .... // this is statically-safe now, thanks to occurrence typing.
                                                          

                                                          To accomplish the same thing in a stricter language, I’d have to make a copy of the underlying array. If I have a deeply nested associate structure, I’d have to write a lot of code to do O(N) work to reconstruct everything. Note that I can also do potentially unsound things like this:

                                                          let isStrings = (x: any): x is string[] => {
                                                            return Array.isArray(x) && (x.length === 0 || typeof x[0] === 'string');
                                                          };
                                                          

                                                          This does O(1) work and defers errors to later if somebody passes ['foo', 5]. When you trust the client, this is super useful for doing the equivalent of pattern matching. And in fact, you can do switch-on-strings, since literal string types are supported:

                                                          let Foo = A | B;
                                                          let Bar = B | C;
                                                          
                                                          interface A { tag: 'A'; blah: boolean; }
                                                          interface B { tag: 'B'; }
                                                          interface C { tag: 'C'; }
                                                          
                                                          let f (x: Foo) => {
                                                            switch (x.tag) {
                                                              case 'A';
                                                                // here it is safe to use x.boolean
                                                                // I can also get exhaustiveness checks here.
                                                               // note that valid cases are A and B, but not C, even though I reuse the B type in two unions.
                                                            }
                                                          }
                                                          

                                                          The syntax isn’t quite as nice as pattern matching, but combined with destructuring, it’s not so bad:

                                                          if (x.tag === 'A') {
                                                            let {blah} = x;
                                                            ...
                                                          }
                                                          

                                                          Ideally, there would be more tools to do more of this in a sound way, rather than rely on you to write correct type assertion functions, but again: They can’t change the semantics of JavaScript. That would require a fair amount of runtime library support for pattern matching and the like. Hard coding some cases like literal string types gets you pretty far.

                                                          All this stuff (and plenty more) enables me to write data representations that make sense to a human: as simple JSON encoded data, with human-friendly grammars as types, reusing data definitions in arbitrary positions, future proofed to new data including additional keys, etc.

                                                          Furthermore, if you believe in the types as parsing intuition, then you’d want your type system to support the same kinds of things that CFGs support: alternation, concatenation, etc. TypeScript isn’t quite there yet with cat, but at least union types let you emulate alt. Cat will probably eventually come in the form of session types in some languages.

                                                1. 1

                                                  What is it about inference rules, regular expressions, and BNF grammars that allow us to understand them perfectly well, even with the syntactical variance? If simple syntactic differences can cause confusion with overlines and substitution, shouldn’t their expression be re-thought entirely? For instance, would the example that mixes overlines and substitution not be clearer if two for-all quantifiers (∀) were used?

                                                  1. 1

                                                    Presumably, if you used a quantifier, you’d still have the problem of delimiting the extent of the quantified variables. As discussed in the talk, you would need to signal grouping with an overline, parens, binding dots, or similar.

                                                    1. 1

                                                      What is it about inference rules, regular expressions, and BNF grammars that allow us to understand them perfectly well, even with the syntactical variance?

                                                      Regular expressions and BNF grammars are easy to understand because they’re just powerful enough for their intended purpose - specifying the syntax of programming languages, which is an easy task provided you don’t purposefully make it hard.

                                                      On the other hand, I’m not sure we understand inference rules that well.

                                                      For instance, would the example that mixes overlines and substitution not be clearer if two for-all quantifiers (∀) were used?

                                                      Not really. The trouble with a substitution like t[xs -> ts] is that it doesn’t really mean what it literally says. You don’t actually have two separate lists xs of variables and ts of terms to substitute them with. You have a single list ss of pairs of the form (x,t), where x is a variable and t is a term to substitute it with. (Alternatively, you may insist on using two separate lists, but then you would need to maintain an invariant that the lists have the same length. IMO, this makes life unnecessarily hard.)

                                                    1. 3

                                                      Just use try! instead of try. It returns nil only if receiver is nil, otherwise it raises NoMethodError.

                                                      “Law of Demeter” is a strange cult which I’ve seen followed only by rubyists. How introducing Payment#client_address reduces coupling? This is just virtual flattening of tree-like or graph-like data structures. Should I define method for each property for each sub-object of Payment? Payment and Client are data classes, they shouldn’t be even viewed from OOP viewpoint.

                                                      I once worked on project where everyone tried to enforce “Law of Demeter” on traversing json in Javascript (more precisely, Coffeescript, and backend was in Ruby).

                                                      1. 4

                                                        If you only want to handle real nil you can use &. – no ActiveSupport needed!

                                                        1. 2

                                                          How introducing Payment#client_address reduces coupling?

                                                          He used the phrase “structural coupling”, which doesn’t immediately mean anything to me, but I can hazard a guess.

                                                          A function that operate on thing.client_address is more broadly usable than a function that operates on payment.client.address, since you may also have a function that operates on contract.client.headquarters.address or similar. This is a general problem that one encounters when you have outbound foreign keys, and becomes more pronounced when refactoring introduces additional indirections. In Clojure, which has namespaced keys, I’ve found it extremely useful to flatten structures when processing events or other somewhat implicitly defined heterogenous collections of entities.

                                                          Consider:

                                                          {:artist/name "Artist"
                                                           :track/name "Track"
                                                           :album/name "Album"}
                                                          

                                                          Is that an Artist object? A Track object? Or an Album object? You can use functions that operate on any of the three!

                                                          Compare to:

                                                          {:track {:name "Track"
                                                                   :album {:name "Album"
                                                                           :artist {:name "Artist"}}}}
                                                          

                                                          That’s obviously a Track object. If I have a function for formatting a track/artist name pair, I could have used it directly with the flattened structure. However, here, I have to traverse the Album edge. If I later extend my data-model to support multiple artists per album (adding an :artist key to a Track object), I’d have to change my code.

                                                          1. 1

                                                            The “Law of Demeter” is also heavily pushed by Bob Martin and his vision of TDD-based Java development. I want to say I’m a fan, but like so many things it’s all about the right tool or methodology for the right job, and the LoD isn’t always the best for speed or clarity.

                                                          1. 4

                                                            As a certified PL amateur, this is very impressive.

                                                            Most async implementations end up bifurcating code into async/non-async flavors (much like const in C++), or require a specialized VM with different callstack semantics. I’m a big fan of async/await overall, but it still sometimes felt like something the compiler should help me more with. What’s more, I can’t imagine using async/await without a dynamic type system: you have to keep a clear mind on whether you’re dealing with a value, or the thunk returning a value. As the paper mentions, most async/await implementations are kind of one-off in the sense they’re hardwired into the language, rather than being building blocks.

                                                            Also, I love the phrase “islands of abstraction” to concisely describe the phenomenon that occurs when you need duplicate versions of functions for different contexts, be they monadic, async, const, etc.

                                                            1. 3

                                                              I totally agree that this is a nicer syntax, and the right one for C++. However, it’s worth noting that it is not without tradeoffs.

                                                              Most async implementations end up bifurcating code into async/non-async flavors

                                                              That’s still happening here, but luckily, mostly behind the scenes: You need to compile resumable and not-resuable functions separately. The templating and constexpr machinery of C++ is being used to accomplish that. See the notes in this proposal about separate compilation. You either need to do one of these things:

                                                              1. Forfeit “up-and-out” resumability; switching to the “down” model by installing a scheduler (ie the spawn function)
                                                              2. Forfeit separate compilation by exposing the definition of your function in a header file.
                                                              3. Forfeit implicit call-site resumability; exporting a function that returns a resumable and executing it at the call site.

                                                              C++ can get away with this because it already has a source compatibility model instead of a binary compatibility one. Template libraries are explicitly not separately compiled. On the other hand, C#, or indeed C++/COM on Windows, where async/await originated, needs binary compatibility and full separate compilation.

                                                              You could encode resumability in to the existing module ABI, either with a distinguished type (like Promise) or with metadata attributes, and then make it implicit statically. However, if you do that, then older compilers wouldn’t be aware when calling things that return Promise or Task or whatever. Upgrading your compiler would create a situation where source code is broken absent call-site level hints to opt-in or opt-out of the type-aware implicit behavior. “await” is basically that hint, opt-in. This isn’t a problem in C++ because you need the newer compiler to call resumable functions in the first place!

                                                              To me, this whole situation sucks. We just need to bite the bullet and have lightweight threads in our operating system and/or language runtimes. There’s lots of challenges there, but it seems to be the only path forward that doesn’t simply layer more complexity on top of our existing type systems.

                                                              What’s more, I can’t imagine using async/await without a dynamic type system

                                                              I assume you meant a static type system? In that case, yeah, I agree: The bifurcation makes it truly annoying to keep T vs Promise straight. However, dynamic types is also a reason to justify the explicit async/await syntax. JavaScript couldn’t accomplish the same thing as C++ because it lacks static information necessary to perform the resumable rewrite during compilation.

                                                              One other thing worth mentioning is there is some prior art worth studying here:

                                                              “Implementing First-Class Polymorphic Delimited Continuations by a Type-Directed Selective CPS-Transform” - Rompf, et al
                                                              https://pdfs.semanticscholar.org/5649/bbc4cc8392918e19fbdb846872130d6fec67.pdf

                                                            1. 3

                                                              Immutability. (…) This means you’ll tend to program with values, not side-effects. As such, programming languages which make it practical to program with immutable data structures are more REPL-friendly.

                                                              Top-level definitions. Working at the REPL consists of (re-)defining data and behaviour globally.

                                                              These two points are in furious contradiction, since redefining top-level definitions is pretty much the ultimate side effect. Every other top-level function can see this “action at a distance”.

                                                              Let me be perfectly clear: ML and Haskell allow you to program using Lisp’s “every definition is subject to revision” style. Just stuff all your top-level definitions into mutable cells. The reason why it’s not done as frequently as in Lisp-land is because, perhaps, perhaps, this is actually a bad idea.

                                                              1. 6

                                                                redefining top-level definitions is pretty much the ultimate side effect.

                                                                Funny, I’d consider the alternative, restarting a process, to be “the ultimate side effect”.

                                                                The reason why it’s not done as frequently as in Lisp-land is because, […]

                                                                Because defaults matter. Wrapping every definition with performUnsafeIO/readMVar would be prohibitive ceremony.

                                                                perhaps, perhaps, this is actually a bad idea.

                                                                Why? My experience is that it’s a really great idea.

                                                                1. 2

                                                                  Funny, I’d consider the alternative, restarting a process, to be “the ultimate side effect”.

                                                                  How so? There’s no local reasoning about the old process being defeated, precisely because you have discarded the old process.

                                                                  Wrapping every definition with performUnsafeIO/readMVar would be prohibitive ceremony.

                                                                  It pales in comparison to the ceremony of re-proving a large chunk of your program correct, because a local syntactic change had global consequences on the program’s meaning.

                                                                  Why? My experience is that it’s a really great idea.

                                                                  Because… how do you guarantee anything useful about something that doesn’t have a stable meaning?

                                                                  1. 3

                                                                    There’s no local reasoning about the old process being defeated

                                                                    Does your program exist in isolation? Useful programs needs to deal with stateful services, such as a database. Why shouldn’t your compiler/language/runtime offer tools for dealing with the same set of problems?

                                                                    It pales in comparison to the ceremony of re-proving a large chunk of your program correct

                                                                    I don’t understand this point at all. If your “proof” is a type checker, then just run the type checker on the new code…

                                                                    how do you guarantee anything useful about something that doesn’t have a stable meaning?

                                                                    You don’t. You stop changing the meaning when you want to make guarantees about it. You don’t need all guarantees at all times, especially during development.

                                                                    Again, consider stateful services. Do you run tests against the production database? Or do you use a fresh/empty database? If you need something to stand still, you can hold it still.

                                                                    1. 2

                                                                      Useful programs needs to deal with stateful services, such as a database.

                                                                      When they’re running, not when I’m writing them.

                                                                      Why shouldn’t your compiler/language/runtime offer tools for dealing with the same set of problems?

                                                                      It isn’t immediately clear to me what kind of problems you’re thinking of, that are best solved by arbitrarily tinkering with the state of a running process.

                                                                      If your “proof” is a type checker, then just run the type checker on the new code…

                                                                      It is not. Some things I prefer to prove by hand, since it takes less time.

                                                                      You don’t need all guarantees at all times, especially during development.

                                                                      It’s during development that I need those guarantees the most, since, after a program has been deployed, it’s too late to fix anything.

                                                                      1. 2

                                                                        When they’re running, not when I’m writing them.

                                                                        Your production system is always running. Is it not?

                                                                        what kind of problems you’re thinking of, that are best solved by arbitrarily tinkering with the state of a running process

                                                                        Who says it is “arbitrary”? I very thoughtfully decide when to change the state of my running processes. When you change one service out of 100 in a production environment, is that not “rebinding” a definition? Why should programming in the small be so different than programming in the large?

                                                                        Have you ever worked on a UI with hot-loading? It’s so nice to re-render the view without having to re-navigate to where I was. Or to write custom code to snapshot and recover my state between program runs.

                                                                        What about a game? What if I want to tweak a monster’s AI routine without having to find through a dozen other monsters to get to the exact situation I want to test? I should be able to change the monster’s behavior at runtime. Great idea to me.

                                                                        Proof is useful, but it’s not everything, and it’s not even clear that it’s meaningfully harmed by having added dynamism. Instead of proving X, you can prove X if static(Y).

                                                                        1. 5

                                                                          This thread is a direct illustration of the incommensurability between the systems paradigm and the programming language paradigm, as described in Richard Gabriel’s “The Structure of a Programming Language Revolution” https://www.dreamsongs.com/Files/Incommensurability.pdf

                                                                          1. 1

                                                                            What a wonderful piece of writing. Thanks a lot for sharing this.

                                                                    2. 1

                                                                      I’d just like to chip in here with one word

                                                                      “Facebook”

                                                                      The Facebook “Process” is a great example of something that never needs restarting but rather features and services are changed or added to the application while it is running in front of our eyes - no page refresh required in the Web version at least.

                                                                      I’d say that from a customer’s perspective this is a really good thing, and continuous end-user improvement is the long tail of continuous integration where nobody need do a reinstall ever again.

                                                                      I realize that this is largely philosophical at this point, but we are starting to have the tools to make this possible in a more general setting.

                                                                      For me as a user it’s a good thing I think, since I don’t need to lose context in huge point releases. Oh look, edit mesh just appeared on my toolbar. I wonder what that does?

                                                                      So I guess I’m with Brandon on this one.

                                                                  2. 2

                                                                    Immutability here refers to data and how the state is managed in the application. Since Clojure uses immutable data structures, majority of functions are pure and don’t rely on outside state. This makes it easy to reload any functions without worrying about your app getting in a bad state.

                                                                  1. 7

                                                                    Context should go away for Go 2

                                                                    I (mostly) agree with this overall thesis, but wish to add some more ahem context.

                                                                    idea that context should carry a map of meaningless objects to meaningless objects

                                                                    It’s actually a pretty clever idea. Coupled with unexported types and the fact that contexts are not enumerable, it lets you hide some context keys to prevent people from reading from that.

                                                                    type foo struct{}
                                                                    var fooKey = &foo{}
                                                                    
                                                                    function WithFoo(ctx context.Context, s string) context.Context {
                                                                      return context.WithValue(ctx, fooKey, s)
                                                                    }
                                                                    
                                                                    function GetFoo(ctx context.Context) string {
                                                                      return ctx.Value(fooKey).(string)
                                                                    }
                                                                    

                                                                    The exported WithFoo/GetFoo functions are just examples. If you don’t export anything, your context key can safely pass through other people’s code and they will never be able to access your private context data.

                                                                    it’s prone to name collisions.

                                                                    This is impossible if you use the pattern I showed.

                                                                    That’s the only problem the “context” package really solves (or attempts to solve)

                                                                    It’s also generally useful for any thing you might use dynamic scope or thread locals for in languages that support those. Yes, I know it’s technically lexical, but it tends to be used in a dynamically-scoped way.

                                                                    For example: Threading request id or current user all the way through for logging contexts.

                                                                    Go 2 should explicitly address the cancelation problem

                                                                    I agree with that. I’m a fan of Martin Sustrik’s structured concurrency idea, but I’m not sure how it would work out when retrofitted in to Go. Explicit termination of subprocesses, a la Erlang, is the way to go.

                                                                    Context is like a virus

                                                                    Yup. It’s also true of any side effects, or indeed any dynamically scoped constructs, such as panic/defer/recover. Go already mandates a stack-frame associated context object for panics, debugging, etc. Maybe it should just give up and expose the current lexical and/or dynamic context to all functions implicitly?

                                                                    1. 3

                                                                      Turned my machine into a space heater, but worth it!

                                                                      Truly stunning detail, but the bill runs high. Some folks have been working on breakthroughs, but we still have a ways to go before it will replace triangles and texture maps.

                                                                      1. 1

                                                                        before it will replace triangles and texture maps

                                                                        I’m sure point clouds will become more feasible and popular, but I don’t think it ever will replace anything. Graphics will always be a vector/raster hybrid. Point-clouds simply invert the dominance from from vector to raster. Both techniques, with their respective dominate end of the spectrum have a place for distinct use cases, levels of abstraction, and resource demands.

                                                                      1. 4

                                                                        I’ve through the blog post, the associated proposal, some of the draft specs, and a bunch of other stuff.

                                                                        Nowhere do I see anything that motivates this. Why is this feature being added? What problem does it solve? Why is the leading underscore convention insufficient? The draft specs suggests that there is significant new complexity added to various core algorithms in the object system, but it feels unjustified.

                                                                        Is it just assumed that “encapsulation” is good and therefore necessary? Is it further assumed that enforced encapsulation is a requirement? Is there speculation that JavaScript may be eventually object-capability secure? Is that even a feasible goal? Would it even be useful in the absence of process abstractions? Feels like more skyscrapers on foundations for sandcastles.

                                                                        1. 3

                                                                          Apparently you overlooked the proposal’s FAQ answer “Why is encapsulation a goal of this proposal?”, which is linked from the middle of the article. This is the proposal’s reasoning:

                                                                          Library authors have found that their users will start to depend on any exposed part of their interface, even undocumented parts. They do not generally consider themselves free to break their user’s pages and applications just because those users were depending upon some part of the library’s interface which the library author did not intend them to depend upon. As a consequence, they would like to have hard private state to be able to hide implementation details in a more complete way.

                                                                          1. 1

                                                                            I agree with this overall sentiment. It seems un-necessary.

                                                                            Perhaps there is a performance angle to this somehow? Since external code can’t reference these fields, yada yada yada

                                                                            I feel like Javascript has hit a pretty good spot in terms of utility with recent revisions, at least for front-end work. I understand more people want to get this stuff into the backend as well, but do we really need runtime support for this? Typescript could surely offer you similar functionality on a static level.

                                                                            I want to know the reasons

                                                                            1. 1

                                                                              Unlike the leading underscore convention, it prevents access to private state from outside the object. In some ways it is a formalization of the leading underscore convention. A leading underscore is just a hint that can be abused intentionally or accidentally, without any error being indicated.