Saher's page

Against methods

Most programming languages nowadays offer some form of method syntax, allowing you to associate a function with a type, so you can call foo.do_thing() as opposed to do_thing(foo). Although commonly used by object oriented programming (OOP) languages, many languages not typically associated with OOP offer method syntax as well. An example that comes to mind is zig, which the creator describes as a procedural C-like language.

Why even offer method syntax if it’s just syntactic sugar? Common reasons:

Command::new("sh")
    .arg("-c")
    .arg("echo hello")
    .output()
    .expect("failed to execute process")

But Methods aren’t without their downsides. The C3 programming language has methods, but the documentation warns against abusing them:

Methods are customarily associated with Object-Oriented programming. In this style one will often encounter code like some_object.run_everything(). C3 is not accommodating to this style, instead one should prefer task::run_everything(some_object). Both the standard library and the design of the language instead follow the principle that functions are used whenever the system is mutating global data, whereas methods are used for mutating a particular value, or extracting data from it. foo.add(bar), foo.to_list() and foo.push(x) are all good uses of methods. On the flip side, methods usage like context.parse_data(data), game.run(settings) and url.make_request() are emphatically not recommended.

Notice how url.make_request() forces the module that defines the URL data type to be aware of how to make network requests. Notice how game.run() suggests that your game’s codebase is architected as a tree of objects with a god-object as the root. A generic context.parse_data(data) must be aware of all possible parsing rules and formats.

C3 discourages methods that touch the outside world or are concerned with something beyond trivial data transformation because they push you towards bad architectural commitments. How many types have you seen that held a reference to an object just for the pursuit of dot syntax? Note that isn’t a critique of OOP itself. It’s about the subtle costs of methods as a feature. C3 itself is not an OOP language.

To illustrate the point further, Consider a document Doc data type. Imagine you wanted to save a Doc, so you define a doc.save(path) method. After a while, you realize you want to save documents remotely into an S3 bucket or whatever, but if you do doc.save_to_s3(config) or bucket.save_doc(doc) you make documents or buckets know about each other! That’s a (bad) architectural decision. It’ll be saner to separately define a save_doc_to_s3() function that lives in a separate module and takes the necessary parameters without need for architectural commitments.

Later, you might decide to replace save_doc_to_s3() by exposing a stream representation doc.write_stream(s) that can be used in any generic storage service. You can still use save_doc_to_s3 if S3 is annoying and requires metadata or whatever. Notice how you can use free form functions without guilt because they are consumers of Doc and can do whatever they want. They let you relocate your assumptions and decisions to a less critical place.

The idea isn’t that methods are inherently harmful (C3 has them despite warning against them), but rather a method is a commitment about what a module should be aware of. If the point is still not clear, consider this comical example with method maximalism.

doc.convert_to_pdf()
doc.share(user)
doc.start_live_edit()
doc.spell_check()
doc.integrity_check()
doc.check_for_plagiarism(config)
doc.convert_to_html()

Can you imagine a single module/package/library providing that much functionality? The module that defines doc is now aware of basically all of computer science! Those methods should be turned into free-form functions that can live in the appropriate place and ask for any extra data types they need

The Odin programming language only offers functions (they’re called procedures in Odin land), which makes procedure names a little more verbose, but it completely removes the risk of abusing methods. You never have to bikeshed whether or not a certain procedure belongs to a data type. The Odin standard library is straight forward, easy to read, and mostly flat.

A minor point that’s often neglected is that methods make the code less grepp-able. I sometimes use grep or ctags to lookup symbols. I know there are fancier tools out there like LSP, but having your code work well with simple tools / no tools is a legitimate boon. Karl Zylinski discusses this in a blog post. You also don’t need to sacrifice a portion of your RAM for the privilege of knowing what x.add() is referring to. I bet you too experienced the sense of dread of watching a malfunctioning LSP server / IDE torture your machine.

I hope this post helps you realize the implications of methods. I believe we’ve all witnessed method abuse because it’s usually tempting and convenient to use them instead of free-form functions, but we ought to be more mindful about the consequences: A method commits the implementation of a function to the module that defines the type.