Call-by-push-value is an evaluation strategy that determines when arguments to functions are evaluated. Call-by-value is what every mainstream language does: arguments are evaluated before the function is called. Call-by-name substitutes arguments directly into a function, so they may be evaluated multiple times or not at all. For example, the following pseudocode:

function foo(n, m) {
    sum = 0
    for i in 1 to 4 {
        sum = n + sum
    }
    if false {
        print(m)
    }
    print(sum)
}

foo({print("1"); 2}, {print("3"); 4})

evaluated with Call-by-Value prints:

1
3
8

evaluated with Call-by-Name prints:

1
1
1
1
8

Call-by-push-value combines both by having two “kinds” of parameters: values which are evaluated immediately (call-by-value), and computations which are substituted (call-by-name). So the following code:

function foo(value n, computation m) {
    sum = 0
    for i in 1 to 4 {
        sum = n + sum
    }
    if false {
        print(m)
    }
    print(sum)
}

foo({print("1"); 2}, {print("3"); 4})

would print

1
8

The reason call-by-push-value may be useful is because both call-by-name and call-by-value have their advantages, especially with side-effects. Besides enabling programmers to write both traditional functions and custom loops/conditionals, CBPV is particularly useful for an IR to generate efficient code.

Currently, Scala has syntactic sugar for by-name parameters, and some languages like Kotlin and Swift make zero-argument closure syntax very simple (which does allow custom loops and conditionals, though it’s debatable whether this is CBPV). Other languages like Rust and C have macros, which can emulate call-by-name, albeit not ideally (you have hygiene issues and duplicating syntax makes compilation slower). I don’t know of any mainstream work on CBPV in the IR side.

    • jeffhykin@lemm.ee
      link
      fedilink
      arrow-up
      2
      ·
      edit-2
      9 months ago

      I think it has to do with how it can be evaluated.

      func1( 1, ()=>console.log("hi")||10 )

      In JS the second argument might be treated as both a computation and a value.

      • console.log(m.toString()) as a value (will print out ()=>console.log("hi")||10)
      • console.log(m()) as a computation

      I think CBPV forces one or the other, which helps the compiler know how to optimize it. If it were just a computation, it wouldn’t need as much overhead as a full function definition I suppose.