NHacker Next
  • new
  • past
  • show
  • ask
  • show
  • jobs
  • submit
Methods in Languages for Systems Programming (2023) (blog.xoria.org)
mrkeen 12 hours ago [-]
The benefit of values over classes is that they do not hide machinery.

If you x.getY(), is that a member access, a computed value, or a network fetch? Will it lazy-instantiate a log4j singleton and start spamming my IDE with red text? I refuse the OO dogma that x should be a black box, and I as the programmer on the outside should not concern myself with its implementation details.

Onto TFA's parser example:

  typedef struct Parser {
      // fields
  } Parser;

  Statement  parser_parse_statement(Parser *p);
  Expression parser_parse_expression(Parser *p);
  void       parser_expect(Parser *p, const char *message);
  void       parser_next(Parser *p);
  TokenKind  parser_current(const Parser *p);
> It sounds like “data should not have ‘behavior’” is being violated here, no? If not, what’s the difference between this module and a class?

Correct. You can write OO in any language, and this parser hides machinery and is acting like a class, not a value. Look at those voids - those methods either do nothing, or they modify the insides of the parser. I certainly can't combine them like I could with values, e.g.:

  Parser *p = choice (parse_statement, parse_expression);
The above cannot work - whose internal machinery should choice adopt?

It is much easier to parse without hidden machinery. Combine them like values, e.g.:

  Parser parser_block   = many (parse_statement);
  Parser parser_sum     = seperateWith (parser_plus, parser_product)
  Parser parser_product = separateWith (parser_times, parser_value)
  Parser parser_value   = choice (parser_literal, parser_variable)
No voids, no internal machinery, any parser_literal is the exact same thing as any other parser_literal.

Then just unhide the internal cursor or success/failure state (or whatever other machinery is hidden in there) and pass them in as values:

  (result, remainder_string) = run_parser (parser_block, input_string);
12_throw_away 1 days ago [-]
IMO the best arguments for using methods have nothing to do with language semantics, but are just grug-brained "this makes my life easier" stuff. Methods are great because they work unreasonably well with autocomplete, they're very easy to find in API docs, and they let you write easily readable call chains without a 10 nested levels of parentheses that need to be read inside-out. Plus, as the article says - it's sometimes nice to have another code organization tool at your disposal.
dfawcus 17 hours ago [-]
Alef [1], [2], [3] can in part be viewed as C where methods are available.

An 'aggr' is equivalent to C 'struct' combined with 'typedef', and an 'adt' is an 'aggr' having methods.

[1] https://en.wikipedia.org/wiki/Alef_(programming_language)

[2] http://doc.cat-v.org/plan_9/2nd_edition/papers/alef/ref

[3] http://doc.cat-v.org/plan_9/2nd_edition/papers/alef/ug

stmw 1 days ago [-]
Good blog post, good balance. One thing to add is that in systems programming, very often the struct is not arbitrarily defined by the programmer - it may be defined by the hardware or another system entirely. So the Data, including its bit-by-bit layout, is primal. It kind of makes sense to have procedures to operate on it, rather than methods.
cv5005 1 days ago [-]
Not a fan of methods.

Why should the first argument be so special? And how do you decide which struct should get method if you have a function that operates on two different types?

nh23423fefe 23 hours ago [-]
Any parameter list is permutation invariant. the first argument is special because methods are members of classes and thus get private access in the body of the method. If the implementation of the method is improved with private access then make a design decision.

You don't have to decide where to put the implementation if you dont want to, its just a type of inlining

    void foo(Type1 arg1, Type2 arg2) {
        // do something
    }

    class Type1 {
        void foo(Type2 arg) {
            foo(this, arg);
        }
    }


    class Type2 {
        void foo(Type1 arg) {
            foo(arg, this);
        }
    }
vs

    class Type1 {
        void foo(Type2 arg) {
            // do something
        }
    }

    class Type2 {
        void foo(Type1 arg) {
            arg.foo(this);
        }
    }
tmtvl 20 hours ago [-]
The first argument isn't special, though:

  (defmethod binary-search (object (collection Sequence) before?)
    ...)

  (defmethod binary-search (object (collection Vector) before?)
    ...)
...and so on. You could even specialize on everything but the first argument.
miguel_martin 24 hours ago [-]
'We believe that data and code should be separate concepts; data should not have “behaviour”.' is flawed, but I don't believe that's the point being made. Instead, I believe the point actually roots in a "programmer mindset" thing when using methods/member functions, due to this explicit separation of data and procedures. With methods/member functions you naturally fall into an "individual element" mindset, see https://www.gingerbill.org/article/2026/01/02/was-it-really-... - yes it's semantically equivalent (given the examples in the article and many other cases), but humans are humans and they are biased.

In my opinion: there is a better argument for making new languages not have methods, or more accurately member functions (as what the author describes).

Consider the following situation: you are user of a library that declares a type called SomeType which has "methods" (member functions) in it. You want to add more "methods" to this type.

Now, there is a problem regarding consisteny w.r.t syntax, your new "methods" now have to be called via `foo(&bar)` instead of `bar.foo()`. You as a user of the library and language have to choice to make (regarding code style):

1. Accept this difference in syntax. Maybe you like this style difference, because now you can clearly see what procedures are declared in your codebase vs. the library's codebase, or:

2. Use freeform functions everywhere. Well actually, you can't do this without a refactor of the library (or with additional language features), i.e. you will need to fork the library and rewrite SomeType and the associated member functions in this freeform function/procedure style.

From a language designer's perspective, you can choose to solve the problem by either (a) forcing the declaration of procedures to be consistent or (b) introducing language features to make the calling code consistent. Odin obviously chose (a), but languages like Swift and C# chose (b) - whereas languages such as Nim chose both (a) & (b).

For (b), here's some possible features you could add:

* Extension methods (Swift, C#). This let's user declared "methods" feel like like "proper methods" of the class/struct, or

* UFCS (Nim, D). Declare everything as a freeform procedure/function but enable the syntax `arr.push(10)` when the procedure is declared as `proc push(arr: var Array, x: int)`

From this, you can see why languages such as Odin chose to go with option (a). It's simpler.

Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact
Rendered at 22:03:25 GMT+0000 (Coordinated Universal Time) with Vercel.