Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I pass a closure with generics to a function without making that function generic?

I have a function that works with a enum to apply binary functions. This is for an interpreter:

use std::ops::*;

#[derive(Debug, Clone, PartialEq, PartialOrd)]
pub enum Scalar {
    I64(i64),
    I32(i32),
    //many many others
}

pub trait TMath: Add + Mul + Sized {} //mark numerical types
impl<T: Add + Mul> TMath for T {}

fn add<T: TMath>(x: T, y: T) -> <T as Add>::Output {
    x + y
}

pub type NatBinExpr<T: TMath> = Fn(&T, &T) -> T;

I want to do:

let result = bin_op(add, &Scalar::I32(1), &Scalar::I32(2));

but also to make it work for arbitrary binary functions:

let result = bin_op(Scalar::concat, &Scalar::I32(1), &Scalar::I32(2));

However, I haven't found a way to pass the closure without making bin_op generic:

fn bin_op(apply: &NatBinExpr???, x: &Scalar, y: &Scalar) -> Scalar {
    match (x, y) {
        (Scalar::I64(a), Scalar::I64(b)) => Scalar::I64(apply(a, b)),
        (Scalar::I32(a), Scalar::I32(b)) => Scalar::I32(apply(a, b)),
    }
}

Making bin_op generic is not right; bin_op operates on Scalar, but the internal operation is generic.

I originally asked this question on Reddit

like image 507
mamcx Avatar asked Nov 08 '22 01:11

mamcx


1 Answers

There are essentially two distinct ways to talk about function types:

  • pointers: fn(A, B) -> C,
  • traits: Fn(A, B) -> C, FnMut(A, B) -> C, FnOnce(A, B) -> C.

In either case, they are characterized by the arguments and result types.

So, what are the arguments and result types of apply?

It depends.

From your example, we can see that it is FnOnce(T, T) -> T for T in [i64, i32, ...].

This is not one type, this is many types. Therefore it needs not a single function but many functions; or perhaps a function object implementing FnOnce multiple times.


The function object route is only available on nightly, and requires an awful lot of boilerplate (for which macros would help):

#![feature(fn_traits)]
#![feature(unboxed_closures)]

use std::ops::*;

#[derive(Debug, Clone, PartialEq, PartialOrd)]
pub enum Scalar {
    I64(i64),
    I32(i32),
    //many many others
}

pub trait TMath: Add + Mul + Sized {} //mark numerical types

impl<T: Add + Mul> TMath for T {}

struct Adder;

impl FnOnce<(i64, i64)> for Adder {
    type Output = i64;
    extern "rust-call" fn call_once(self, args: (i64, i64)) -> i64 {
        args.0 + args.1
    }
}

impl FnMut<(i64, i64)> for Adder {
    extern "rust-call" fn call_mut(&mut self, args: (i64, i64)) -> i64 {
        args.0 + args.1
    }
}

impl Fn<(i64, i64)> for Adder {
    extern "rust-call" fn call(&self, args: (i64, i64)) -> i64 {
        args.0 + args.1
    }
}

impl FnOnce<(i32, i32)> for Adder {
    type Output = i32;
    extern "rust-call" fn call_once(self, args: (i32, i32)) -> i32 {
        args.0 + args.1
    }
}

impl FnMut<(i32, i32)> for Adder {
    extern "rust-call" fn call_mut(&mut self, args: (i32, i32)) -> i32 {
        args.0 + args.1
    }
}

impl Fn<(i32, i32)> for Adder {
    extern "rust-call" fn call(&self, args: (i32, i32)) -> i32  {
        args.0 + args.1
    }
}

fn bin_op<F>(apply: &F, x: Scalar, y: Scalar) -> Scalar
    where
        F: Fn(i64, i64) -> i64,
        F: Fn(i32, i32) -> i32,
{
    match (x, y) {
        (Scalar::I64(a), Scalar::I64(b))
            => Scalar::I64((apply as &Fn(i64, i64) -> i64)(a, b)),
        (Scalar::I32(a), Scalar::I32(b))
            => Scalar::I32((apply as &Fn(i32, i32) -> i32)(a, b)),
        _ => unreachable!(),
    }
}

fn main() {
    let result = bin_op(&Adder, Scalar::I32(1), Scalar::I32(2));
    println!("{:?}", result);
}

Prints I32(3).

like image 181
Matthieu M. Avatar answered Nov 30 '22 23:11

Matthieu M.