HNHacker News
TopNewBestAskShowJobs

nauticacom

132 karma · joined November 23, 2021

submissionscomments
nauticacom··on Why general inheritance is flawed and how to finally fix it
Sure, that works for some specific problems where you're computing a value from a defined set of data types. "Subset of this data" was an example I've encountered in the past and used here because it has clearly distinct cases—give me the whole thing, give me some index-delimited range, possibly others—but there are plenty of other examples that don't fit a polymorphic function model (and let's forget that I've never even used a language with polymorphic functions).

As another example I've encountered in the past, let's say you have some object that can dynamically define fields. Once you define a field, you can retrieve its value or maybe some default value e.g.

    model = Model.new
    model.define("points", default: 1)
    model.store("points", 10)
    points = model.retrieve("points")
    puts points # => 10
Let's say doing anything with an undefined field is invalid. Here's my first pass at an implementation:

    class Model
        def initialize
            @fields = {}
        end

        def define(name, default: nil)
            @fields[name] = Field.new(name, default)
        end

        def retrieve(name)
            @fields[name].value
        end

        def store(name, value)
            @fields[name].value = value
        end
    end

    class Field
        attr_reader :name
        attr_accessor :value

        def initialize(name, value)
            @name  = name
            @value = value
        end
    end
Works great! One day a requirement comes along that default values need to be lambdas, too, which are called every time the value is retrieved. How do we implement that? One way is to add a conditional to the Field class:

    class Field
        attr_reader :name
        attr_writer :value

        def initialize(name, value)
            @name  = name
            @value = value
        end

        def value
            if value.is_a?(Proc)
                @value.call
            else
                @value
            end
        end
    end
But now Field knows that it can be passed a lambda, so testing it needs to account for that case (among many other considerations, probably, in a real-world system). And any time we add more cases for default values, let alone changes to regular values like type casting or something, the Field class becomes more complicated. I'd probably reach for a new object instead:

    class Model
        def initialize
            @fields = {}
        end

        def define(name, default: nil)
            @fields[name] = Field.new(name, nil, Default.for(default))
        end

        def retrieve(name)
            @fields[name].value
        end

        def store(name, value)
            @fields[name].value = value
        end
    end

    class Field
        def initialize(name, value, default)
            @name    = name
            @value   = value
            @default = default
        end

        def value
            if @value.nil?
                @default.value
            else
                @value
            end
        end
    end

    class Default
        def self.for(indicator)
            if indicator.is_a?(Proc)
                Default::Dynamic.new(indicator)
            elsif indicator.nil?
                Default::None.new
            else
                Default::Static.new(indicator)
            end
        end

        class Static < Default
            def initialize(value)
                @value = value
            end

            def value
                @value
            end
        end

        class Dynamic < Default
            def initialize(callable)
                @callable = callable
            end

            def value
                @callable.call
            end
        end

        class None < Default
            def value
                nil
            end
        end
    end
Now we've changed the conditional in the Field class to one that's actually relevant to it (do I have a value yet?) and won't change when the kinds of default values that it can accept change. Because we dependency-injected the Default object into the Field object, testing that conditional becomes a binary of retrieving the default value when no value is set, and retrieving the value once it's set. We can then test each kind of Default on its own, and changes to Default don't impact Field. If we really, really wanted to we could even eliminate the conditional in Field alltogether by unifying the interface for @default and @value such that they're both objects with a #value method (or maybe rename it to something else so we don't write @value.value). In either case we've made each piece simpler to reason about and pushed conditionals up the call stack so the resulting code is more straightforward.

I can probably recall more examples of simplifications like this, but this is where I find inheritance the most useful: a known set of things that each polymorphically conform to some interface. In these examples I don't actually use the superclass for any shared behavior, but you can imagine a case where I might.

One other benefit that I really like from the inheritance-object-modeling-as-pushing-up-conditionals perspective is that it makes you define what the different cases of something are as distinct objects, and give names to them. It's a similar benefit that falls out of using named sum types instead of signal values or tagged unions or something, but has the opposite effect (overall reduction of conditionals rather than proliferation).

nauticacom··on Old CSS, New CSS (2020)
It's pretty silly to claim that one way or the other is "obviously" wrong. A primary objective of defaults on the web is to never hide content; if "width" specifies the width of the content itself (i.e. box-sizing: content-box), when I say "div { width: 100px }" and put an image that's 100px in that div, I never crop/lose content. You can argue one way or the other, but neither is "obviously" more correct.
nauticacom··on Why general inheritance is flawed and how to finally fix it
I (happily) write a lot of OOP code, "inheritance is bad, use composition" is such a trite and unhelpful dogma that gets in the way of any actual discussion about where inheritance is useful.

IMO, the case where inheritance makes the most sense is when you have a set of objects polymorphically answering some question, usually with a simple answer.

    class Subset
        class Whole < Subset
            def of(items)
                items
            end
        end

        class Range < Subset
            def initialize(from:, to:)
                @from = from
                @to   = to
            end

            def of(items)
                items[@from:@to]
            end
        end
    end
which is used as such:

    subset = Subset::Whole.new
    puts subset.of(["a", "b", "c"]) # => ["a", "b", "c"]

    subset = Subset::Range.new(from: 0, to: 1)
    puts subset.of(["a", "b", "c"]) # => ["a"]
You can then pass around a Subset object anywhere (aka dependency injection) and push conditionals up the stack as far as possible.

Simply saying "inheritance is bad" gets nobody anywhere.

nauticacom··on Rust – Build Your First Web App with Rocket and SeaORM
> The thing though is you can build higher-level abstractions on top of the base language

That's true, but since the base language is so low-level there are certain things you can't get away from.

You can never get away from the fact that String/&str and Vec/&[] are different. If I'm writing code where every allocation counts, then I need a language to be able to, for example, give me a substring without allocating. If I'm writing web code, though, the number of allocations is probably in the thousands or tens of thousands and I just don't care. But using Rust, I have to care about the difference between allocated types and slices, which is unnecessary complexity.

You can never get away from the fact that heap allocations are explicit, so most kinds of dynamism, since they rely on runtime polymorphism, need to go through Boxes or generic types or something else. I can't count the number of time's I've needed to Box something because I needed runtime polymorphism, or needed to rework an interface because I had Boxed something but now it's not object-safe.

You can never get away from Rust's ownership model, which wreaks havoc on graph-like structures that are common in a domain model (which you'll undoubtedly have in your web app/api. It's no big deal for a language with a GC, but because the memory semantics are difficult, Rust forces that difficulty onto you. Again, it would make sense if I only had some tiny amount of RAM to work with on a microcontroller, but I can spare gigs of server RAM.

All of these (and others, these are just examples) add up to complexity that is unnecessary and overwhelming for the level of abstraction that web projects deal with. Rust certainly has many great things, but it's a trade off, and imo it's one that's not worth it.

Personally, I think tools are better when they target a specific audience with specific goals and specific use-cases. I suppose I stretched calling it "unrealistic" for Rust, since, well, look where it is; I guess it's more just a position I don't understand or agree with.

Honestly the best part of Rust is (most of) the community. Most of the people I interact with are incredibly friendly, welcoming, and interested in making the community better. I wish Swift had half the community Rust does, and wasn't so Apple-controlled, because it feels like an applications-friendly version of Rust.

nauticacom··on Notes on the Go translation of Reposurgeon (2020)
Ah, that's interesting! I'm not the person who used "syntactic bureaucracy" but I did interpret it roughly like that, yes. I'm not sure what phrase I would use to describe "more syntactical constructs" as you're saying, but it's interesting how we saw the term differently.

Similarly, even though I'm not a big fan of Rust, I would personally prefer to encounter the Rust snippet. The way I read code, I'm already building up mental models of things in my head, so adding more (e.g. what fold is) isn't that big of a deal to me. I think I'm a person who is able to look at a function call and not have the desire to dig into its source, though, which I don't think is the way everybody (and most certainly not the designers of Go) feels. Plus once I understand the concept, even if it's a lot less universally applicable than fold, I can reuse my understanding of it throughout the system and possibly throughout multiple systems.

Like you said, I think this is largely a subjective thing. I just find it objectionable when people, on either side of the fence, come in and say "abstracting over concepts and possibly making them first class is always better" or "...always worse."

nauticacom··on Notes on the Go translation of Reposurgeon (2020)
> What do you consider syntactic bureaucracy?

I would assume "amount of syntax required to express a given concept," with the use of the word "bureaucracy" implying that some concepts require too much syntax relative to their complexity (something that depends on your values). The classic example being mapping over a slice.

nauticacom··on Notes on the Go translation of Reposurgeon (2020)
Sure, but there's a purpose-built tool that doesn't overload an already existing concept, and it's called keyword arguments
nauticacom··on Notes on the Go translation of Reposurgeon (2020)
I suppose it could be, but imo it starts diluting the value of domain modeling if everything is given the same weight. A bundle of inputs to a function just doesn't have the same weight as a struct representing a repository or a commit or something, so modeling them as the same concept just doesn't sit right with me. It could be a personal thing, though.
nauticacom··on Notes on the Go translation of Reposurgeon (2020)
It's a spectrum, though. A language can be between 0 expressiveness (Go) and 100 expressiveness (Perl, maybe Lisps), and claiming that the only way to avoid the mysterious evil team member who will wreck your codebase is to patronizingly limit them "for their own sake" is insulting
nauticacom··on Notes on the Go translation of Reposurgeon (2020)
Because concepts in programming languages mean things. A discrete, named entity (a struct) is a distinct concept from a way to increase readability of a function call (keyword arguments). Overloading them with the same language construct is compressing disparate concepts. It's the same thing when "Go has no set type" comes up and people say "map[T]struct{}!" You might implement a set using a map, but they're fundamentally different concepts
nauticacom··on Notes on the Go translation of Reposurgeon (2020)
Not really? I suppose what I meant by that was that, if you make a struct, it should represent a concept that makes sense outside of the context of passing it to one function in particular; you're signalling that this collection of data represents a concept in your program
nauticacom··on Notes on the Go translation of Reposurgeon (2020)
Even if you're not doing OOP you're modeling your domain in one way or another. Whether those models are explicit and expressive or not, and how you define those qualities, is another question. In neither case are the arguments to a particular function typically a concept in your domain
nauticacom··on Notes on the Go translation of Reposurgeon (2020)
I thought of Dart as a counterpoint, but if anything it's actually more proof. Dart kinda failed as a language in the browser because Google didn't really push it, and when they did it got pushback. Now that they've repurposed it for building mobile apps, it's surprisingly popular. Sure, not Go-levels of popular, but leaps and bounds more popular than if it were some scrappy OSS project. And it's in a similar camp to Go: reasonably uncontroversial (it's basically Java), large suite of libraries, backed by Google.
nauticacom··on Notes on the Go translation of Reposurgeon (2020)
Sure, if you're pushing things through a command bus or something. Otherwise you're just stretching it. If you wouldn't use it in a sentence to describe a use-case, I'd say that it's probably not a concept.

Would it? Only if you allow default values, which is a separate discussion. And even then only if your only mechanism for default values doesn't let the definitions specify what, exactly, the default value is.

nauticacom··on Notes on the Go translation of Reposurgeon (2020)
I'd agree that it was one of their design goals, but I wouldn't go as far as to say that it makes the language "readable and usable." That depends on what your values are.

Go's philosophy (which clearly flows from its creators being C-enthusiasts) is that the only thing that matters for reading, writing, and understanding a program is what it concretely does, i.e. what structures are created, where values are stored, how computations are performed etc. If that's also your philosophy, then of course it's going to jive with you.

But plenty of people also have different philosophies. Maybe you think the main thing that's important in crafting programs is developing a rich domain vocabulary that expresses concepts and how they interact. Maybe you think that what's important is formal proof of both logical and concrete correctness. In those cases, Go's rigorous opposition to abstraction (coming from its philosophy that what's important is concrete operations) will probably irritate and slow you down.

I couldn't say exactly why it got popular. I'd guess that some significant segment of programmers also share its philosophy, but I have no evidence to back that up. Certainly any reasonably uncontroversial language with a large suite of libraries backed by Google is bound to have some level of popularity.

nauticacom··on Notes on the Go translation of Reposurgeon (2020)
One thing that the people saying "just use a struct for keyword arguments" are missing is that structs should signal intent, i.e. "this is a concrete concept in the system." A Repository, Commit, Message, Person, etc. struct are all concepts in the domain, whereas "the arguments for this particular function" is not. I think Go people are allergic to writing code that does anything other than functionally work.
nauticacom··on Rust – Build Your First Web App with Rocket and SeaORM
It's because Rust's fundamental concerns—ownership, error handling, mutability, etc.—are at a much lower level than most applications need to deal with.

Rust has the (imo unrealistic) goal of trying to fit an incredibly broad range of use-cases, from embedded microcontrollers to command-line tools to web frameworks, each of which has a different level of abstraction and concerns that are important. When you go up the ladder of abstraction like in this project, stack vs heap, ownership, et al get in the way of dealing with application logic.

nauticacom··on Tailwind CSS v3.0
I find it funny whenever Tailwind people tout that you "don't have to name things." You still have to name your JS-framework-of-choice components, and you still have names for each of the parts of a single component that get smattered with class names; it's just that the name is implicit in your head now instead of explicit in the CSS
nauticacom··on uBlock, I exfiltrate: exploiting ad blockers with CSS
Yes, because everybody has willingly surrendered to the horrors of React hegemony
nauticacom··on ImageMagick: CLI for Image Editing
Thankfully as of v7 these are all bundled under a single "magick" command (with symlinks for compatibility). Hopefully in the future these can be removed
nauticacom··on Our journey in dropping the ORM in Go
It's no wonder this article was written in 2006. One of the problems describes DBAs! Has anyone heard of a DBA in the last decade?

Most of these other problems are just... solved? Any decent language will let you override the meaning of equality between two objects, if you don't use a ton of inheritance the inheritance problem won't bite you, good libraries let you describe the schema once and either generate SQL or generate classes, etc.

nauticacom··on Our journey in dropping the ORM in Go
Okay so I write a prepared statement, send it to my database with some parameters, and get back some rows. Now what? How do I turn those rows into structs or objects or whatever that I can pass around for business logic? Maybe I write a function that takes a row and maps it to an object, I call that for every row I get back, whatever. Oh but now I'm joining a belongs-to relationship and I want that joined row represented as an object, too, so I have to write a function to convert that row and oh what if the fields have conflicting names okay I can always prefix them with the name of the table and wait sometimes I'm not querying with this related object so I have to check if the related id is included in the row and... on and on and on for every possible way I could make a different query.

Great, now I've basically reinvented an ORM but it's worse because I have to manually maintain all this mapper code and it only ever accounts for the cases I've considered. If I make it fully generic, then I've really just reinvented an ORM.

nauticacom··on Our journey in dropping the ORM in Go
Right, but for most of that stuff I can just write a one-off query, munge it into the ORM's objects and pass that off to my view for rendering. But probably 80% of my queries are "get this object by ID" or "get these objects and some related ones by a simple condition" or maybe a slightly complex condition. In these cases an ORM works wonders and has almost zero downsides
nauticacom··on Our journey in dropping the ORM in Go
Never understood the hate for ORMs. I need to map from my data storage to my domain model somehow, why write all that code myself
nauticacom··on Show HN: Randomly create beautiful website layouts
Alternate, more accurate title: Create cookie-cutter website layouts that look like every other bland landing page. Bonus points if they use Tailwind.
nauticacom··on Django, HTMX and Alpine.js: Modern websites, JavaScript optional
So cool! I love being able to break accessibility and the fundamental building block of the web :)
nauticacom··on Django, HTMX and Alpine.js: Modern websites, JavaScript optional
If you need to run it through a "transform" step, in this context, it's compiling. What it ultimately gets transformed to, and the implications of that, is a different discussion
nauticacom··on Django, HTMX and Alpine.js: Modern websites, JavaScript optional

  How do YOU do form validations?
I use some server-side library to do it (take your pick for your language/framework of choice) which then sends it back to the user, in most cases automatically wiring up old field values and error messages. Checking string lengths or whatever is equally easy in both cases, but what's really difficult is the part you conveniently hand-wave away:

  usually, it's harder / more awkward to wire the API feedback to highlight the offending Form fields
This is the entire point of validations. When I render everything on the server, I have my entire application and its associated logic to play with; can this Person access that Document? Has this Person already enabled Notifications for new Revisions on this Document? etc. Most server-side libraries make it equally easy to surface validation errors for those questions as they do questions like "Is this Password no shorter than 10 characters." Every time I've tried to do this on the client there's been no easy way or you have to wire it up manually every time.

  There is no difference in how an HTMX app would authenticate to its endpoints and how an SPA would
I've found this not to be the case. Here are the potential options I see:

- The client contains all the logic for who can view what, and redirects or shows a message appropriately using whatever mechanism is appropriate (e.g. Angular has route guards, I'm sure other routers have similar functionality). Because you want real data protection, this logic is also on the server, so now you have duplicated logic and you have to hit the server multiple times on every navigation, which is basically impractical. - The server contains all the logic for who can view what, and when the client asks the server for some data, if it responds with 403 it can show a message or redirect or whatever. This is okay, but now you have so little information on what happened on the client side that it's a much worse experience. e.g. if I try to access a document that I don't have access to on the server-side, I can redirect_back and maybe show a pretty message that says "hey you can't access that, maybe click this link to ask for permission." If I just get a 403 from the server, all I can say is "uhh I don't know, you can't do whatever you just tried to do."

This is much easier if all your logic resides on the server. To check if a Person can access a Document, you just call person.owns?(document) or whatever. If they don't, you just redirect_to wherever, and since you haven't actually rendered anything yet it's all in a single HTTP request-response cycle.

  Obviously, but you load the client version from the server at API request time
I'm not sure you understand my problem here; it seems like perhaps client-heavy people just aren't doing this anymore? Ember, Backbone, et al got this part right, and everything since then seems to have forgotten about the "M" part of MVC.

What I mean is that on the server I have a Document class, a Person class, etc. and the associated business logic for dealing with it. Maybe a document has a bunch of Revisions, and they're stored in an array or something and I can get the newest revision by calling document.latest_revision, or I can get properties of the latest revision by just doing document.latest.title. Maybe when I update a field on a Document I really want to create a new Revision or something. Whatever, I'm just making stuff up here, but you get the kind of business logic I'm talking about.

How do I do any of this on the client? Do I have a separate Document class that maintains all the same logic? Do I split the logic so some of it is on the client and some is on the server? Do I put it all on the client...oh but wait I can't do that because the server needs to do integrity checks or make database calls that the client simply can't do.

In an ideal world, what I want is essentially like my ORM on the server-side: a cache of every instance of every model that I've loaded from the server, normalized with references to one another and an easy way to retrieve an object with its relationships either from memory or from the database (network/API). It's then easy to update Document(id=45) in one place and have it propagate to the rest of the app, and if it's referenced by any related objects to have the update propagate to them, too. This is a hell of a lot of work to create, and basically none of the modern frameworks provide anything resembling this.

---

Your final few paragraphs are essentially all the rosy writing out there about client-heavy approaches: look how wonderful it is! Just fetch your data and we'll rerender it! I understand all that. And maybe it's evidence that we're just living in such different worlds that there's difficulty explaining requirements to one another.

If I was creating Desmos or something, where basically every ounce of logic exists on the client, and the client's job is just to do a bunch of stuff with client-generated data and display it, then yeah I'd probably reach for a client-centric approach. But I'm not. Most of the time I'm creating GitHub or Wikipedia or Fastmail or something, where there could be some nice benefits of having a fat-client that talks to an API, but the massive complexity increase just isn't worth it.

nauticacom··on Django, HTMX and Alpine.js: Modern websites, JavaScript optional

  I've tried these technologies ... and for me they are less productive than the ways I know how to build API-first, JavaScript web apps
I think I feel the same way you do, except the other way around. I genuinely can't understand how you build apps productively with a separate client/server model.

How do you do form validations? How about authentication and authorization? What about a consistent domain model? Is there one copy of it on the server and one on the client? Or does your client just treat everything as dumb data and make network requests for every question of business logic? What about data consistency throughout the interface—when you update e.g. Document(id=45) with new data, how does that update get propagated to every other place in the app? Does that involve a client-side identity-map? Or are you manually trying to send updated data to other places with callbacks/whatever? etc. for ever and ever.

Every time I try to build an app where the frontend and backend are split into a separate client and API, it ends up adding so many more considerations and so much more complexity than the traditional model. Mostly because now everything that just required a database query or method call before now also requires a network request, but also because data consistency in a long-running app is so hard to get right.

← PreviousPage 2 of 2