← Back to context

Comment by mrkeen

6 days 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);