Asked  6 Months ago    Answers:  5   Viewed   18 times

In another Stack Overflow question Leon Timmermans asserted:

I would advice you not to use prototypes. They have their uses, but not for most cases and definitely not in this one.

Why might this be true (or otherwise)? I almost always supply prototypes for my Perl functions, and I've never before seen anyone else say anything bad about using them.

 Answers

74

Prototypes aren't bad if used correctly. The difficulty is that Perl's prototypes don't work the way people often expect them to. People with a background in other programming languages tend to expect prototypes to provide a mechanism for checking that function calls are correct: that is, that they have the right number and type of arguments. Perl's prototypes are not well-suited for this task. It's the misuse that's bad. Perl's prototypes have a singular and very different purpose:

Prototypes allow you to define functions that behave like built-in functions.

  • Parentheses are optional.
  • Context is imposed on the arguments.

For example, you could define a function like this:

sub mypush(@@) { ... }

and call it as

mypush @array, 1, 2, 3;

without needing to write the to take a reference to the array.

In a nutshell, prototypes let you create your own syntactic sugar. For example the Moose framework uses them to emulate a more typical OO syntax.

This is very useful but prototypes are very limited:

  • They have to be visible at compile-time.
  • They can be bypassed.
  • Propagating context to arguments can cause unexpected behavior.
  • They can make it difficult to call functions using anything other than the strictly prescribed form.

See Prototypes in perlsub for all the gory details.

Tuesday, June 1, 2021
 
Guesser
answered 6 Months ago
95

Only perl can parse Perl (see this example):

@result = (dothis $foo, $bar);

# Which of the following is it equivalent to?
@result = (dothis($foo), $bar);
@result = dothis($foo, $bar);

This kind of ambiguity makes it very hard to write source filters that always succeed and do the right thing. When things go wrong, debugging is awkward.

After crashing and burning a few times, I have developed the superstitious approach of never trying to write another source filter.

I do occasionally use Smart::Comments for debugging, though. When I do, I load the module on the command line:

$ perl -MSmart::Comments test.pl

so as to avoid any chance that it might remain enabled in production code.

See also: Perl Cannot Be Parsed: A Formal Proof

Friday, June 18, 2021
 
Octopus
answered 6 Months ago
98

This issue is related to the fact that primitive types in Java are not unified to be substitutable for Object, and with generic type erasure.

Using Function<T, Integer> instead of IntFunction<T> when the last one suffices has 2 disadvantages:

  • Every returned int is boxed - meaning a larger memory footprint;
  • Every returned Integer gets an automatic runtime check (which can be optimized away, but yeah...);

Note that these kinds of issues with the collection framework in Java have led people to write a whole library, named Trove, that eschews the generic interfaces in favor of specialized collection types for every primitive type.

Tuesday, July 6, 2021
 
tompave
answered 5 Months ago
90

Define all the functions so they take a single array argument.

Comment from Barmar

Unifying all functions to the same prototype is exactly what one normally does in this case, though I'd go with a prototype with two parameters: A pointer to an array with the real parameters as well as it's size. That way not every function has to split/parse its arguments on its own.

I really like stuff like this, so I made a short demo. I made this on my mobile, so it's a bit rough and would need some improvements if used in the wild (memory management and error detection for example). Here it is:

#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <string.h>

// a node in the abstract syntax tree. Either a
// value or a call
struct Ast {
  bool isCall;
  union {
    int value;
    struct {
      char const * operator;
      size_t countOperands;
      struct Ast * operands;
    } call;
  };
};

// unified function type. Could've also passed an
// int array, but then evaluate would've needed
// a memory allocation, so ...
typedef int (*Function)(struct Ast *, size_t);


// implementation of + function. Sums the values of
// parameters. (which are hopefully evaluated)
int sum(struct Ast * parameters, size_t num) {
  int result = 0;
  while (num > 0) {
    --num;
    result += parameters [num]. value;
  }
  return result;
}

// implementation of ? function, ignores any
// parameters and just asks for an integer.
int ask (struct Ast * parameters, size_t num) {
  int value;
  scanf("%d", & value);
  return value;
}

// poor man's lookup table
static Function const functions [] = {sum, ask};
static char const * const function_names [] = {"+", "?"};

// poor man's lookup from above static arrays
Function lookup (char const * name) {
  size_t it = sizeof (functions) / sizeof (functions [0]);
  while (it > 0) {
    --it;
    if (strcmp(name, function_names [it]) == 0) {
      return functions [it];
    }
  }
  exit(1);
}

// evaluate an Ast. Normally one wouldn't return
// an Ast node but rather some value_t (assuming
// dynamic typing)
// this function is also destructive on call Ast nodes,
// in order to get around any memory management.
// so be careful!
struct Ast * evaluate (struct Ast * node) {
  if (! node->isCall) {
    // anything that's not a call is a value, thus
    // self evaluating, return it unchanged!
    return node;
  }
  // so it's a call. Get the associated function from
  // the lookup table!
  Function f = lookup(node->call.operator);
  // unconditionally evaluate all operands of the call.
  // thus no macros or conditionals, sorry!
  size_t o;
  for (o = 0; o < node->call.countOperands; ++o) {
    // destructive!
    node->call.operands[o] = *evaluate(&(node->call.operands[o]));
  }
  // use the call node to store the result value.
  // this will blow up if any call node uses any
  // allocated memory!
  node->isCall = false;
  // call the function with the evaluated operands and
  // store the result
  node->value = f(node->call.operands, node->call.countOperands);
  return node;
}

int main () {
  // I didn't want to write a parser, so here's a
  // static Ast of (+ 21 10 (?))
  struct Ast nodes [] = {
    {.isCall=false, .value=21},
    {.isCall=false, .value=10},
    {.isCall=true, .call = {
        .operator="?", .countOperands=0}},
    {.isCall=true, .call = {
        .operator="+", .countOperands=3,
        .operands=nodes}}};
  struct Ast * result = evaluate(&(nodes [3]));
  printf("(+ 21 10 (?)) => %dn", result->value);
  return 0;
}

Written and "tested" on ideone.

A different approach would be to use a void * tagged with some function type information. But it's rather difficult to pass the actual parameters to functions encoded like that, and it also doesn't scale well.

Sunday, August 22, 2021
 
pawello2222
answered 4 Months ago
65

Yes, it's valid. In this example, ... creates a variadic function using the va_list mechanism. This is how variadic functions are implemented in C, and to some degree in C++ (though C++11's template parameter packs have rendered this mechanism obsolete).

Further reading: va_arg

Friday, September 3, 2021
 
zi3guw
answered 3 Months ago
Only authorized users can answer the question. Please sign in first, or register a free account.
Not the answer you're looking for? Browse other questions tagged :  
Share