clr

taryn 2025-01-09T11:51:56.017689Z

Good day all. What must I do to be able to use a C# Func<I, O> (as a Clojure IFn) from within Clojure? The following is the code I'm running and the output (exception) I am getting when the fn is invoked.

using ClojureLang = clojure.lang;
using Clojure = clojure.clr.api.Clojure;

namespace ClojureClrExploration
{
    class Program
    {
        static void Main()
        {
            Func<object, object> printObject = (object o) => {
                System.Console.WriteLine($"{o}");
                return o;
            };

            ClojureUtils.internVar(
                "explorize.api",
                "prn-obj",
                printObject,
                replaceRoot: true
            );

            ClojureUtils.require("explorize.entrypoint");
            ClojureUtils.invoke(
                "explorize.entrypoint",
                "main"
            );
        }
    }

    static class ClojureUtils
    {
        static ClojureLang.IFn _require = Clojure.var("clojure.core", "require");
        static ClojureLang.IFn _deref   = Clojure.var("clojure.core", "deref");

		public static void internVar(string ns, string n, object val, bool replaceRoot)
		{
            ClojureLang.Var.intern(
				ClojureLang.Namespace.findOrCreate(ClojureLang.Symbol.intern(ns)),
				ClojureLang.Symbol.intern(n),
				val,
				replaceRoot
			);
		}

        public static void require(string ns)
        {
            System.Console.WriteLine($"(require '{ns})");
            _require.invoke(Clojure.read(ns));
        }

        static dynamic deref(string ns, string n)
        {
            return deref(Clojure.var(ns, n));
        }

        static dynamic deref(ClojureLang.IFn aVar)
        {
            return _deref.invoke(aVar);
        }

        public static object invoke(
            string ns,
            string n
        ) {
            return deref(ns, n)
                .invoke();
        }
    }
}
These Clojure source files are in a subdirectory named explorize:
;; api.clj
(ns explorize.api)
;; entrypoint.clj
(ns explorize.entrypoint
  (:require [explorize.api :as e.api]))

(defn main
  ([]
   (e.api/prn-obj nil)))
$ CLOJURE_LOAD_PATH="$(pwd)/explorize" dotnet run
(require 'explorize.entrypoint)
Unhandled exception. System.InvalidCastException: Unable to cast object of type 'System.Func`2[System.Object,System.Object]' to type 'clojure.lang.IFn'.
   at explorize.entrypoint$main__20233.invokeStatic()
   at explorize.entrypoint$main__20233.invoke()
   at System.Dynamic.UpdateDelegates.UpdateAndExecute1[T0,TRet](CallSite site, T0 arg0)
   at ClojureClrExploration.ClojureUtils.invoke(String ns, String n) in Program.cs:line 70
   at ClojureClrExploration.Program.Main() in Program.cs:line 23

jamesd3142 2025-01-10T00:32:05.292099Z

Maybe you just want

;; entrypoint.clj
(ns explorize.entrypoint
  (:require [explorize.api :as e.api]))

(defn main
  ([]
   (.Invoke e.api/prn-obj nil)))
or
;; entrypoint.clj
(ns explorize.entrypoint
  (:require [explorize.api :as e.api]))

(def prn-obj' #(.Invoke e.api/prn-obj %))

(defn main
  ([]
   (prn-obj' 123)))
? But I'm not really sure what you are trying to achieve.

jamesd3142 2025-01-10T00:32:12.191769Z

Possibly you are looking for https://insideclojure.org/2024/02/12/method-values/ which is a ClojureJVM 1.12 feature that has not been added to ClojureCLR yet.

dmiller 2025-01-10T04:48:38.656339Z

Method values are in the latest alpha of ClojureCLR. I don't think this mechanism would solve the problem as stated. (Along with @jamesdavidson, I'm not sure your exact intent.) There is a way in ClojureCLR to generate a Func<...>: (sys-func [Object Object] [x] ...something ...) No one ever asked to go the other way. My first thought is any such mechanism is just going to end up calling Invoke internally anyway. The proposed solution seems simplest.

dmiller 2025-01-10T13:37:51.586809Z

If you just want to create a 'function' on the C# side that can be consumed as as a 'function' on the ClojureCLR side, then it might be better to create a class that implements the IFn interface and pass along an instance of that class. Rather than dealing with IFn directly, it is much more convenient to base your class on clojure.lang.AFn or clojure.lang.AFunction. The former provides default implementations for all the overloads of invoke and for applyTo. The latter adds support for metadata and IComparer. Create a class derived from one of those and override the invoke method(s) of interest. AFn also implements interface IFnArity with method bool HasArity(int arity) which you should override if you want to play nice. For your case, this would look something like

class MyFunc : AFn
{
    public override object invoke(object arg1)
    { 
        Console.WriteLine($"{o}");
        return o;
    }
   
    public override bool HasArity(int arity)
    {
        return arith == 1;
    }
}
You can also provide additional data fields and constructors to match to provide the equivalent of closures. BTW, if you work in F#, you can use object expressions for this:
let f = { new AFn()
              // the object expression syntax requires some thing here,  
              member _.ToString() = "Hello, it's me."   // Todd Rundgren. I prefer the 1972 version
           interface IFn with
               member _.invoke(arg1) = Console.WriteLine($""o}"); o
         }
This will also allow you to close over locals directly. If you want a less bespoke solution in the early days I did provide a general mechanism to passing along Func<...> objects to be used as IFns . For some reason, I defined my own replacement for Func<> -- something to do with how I was interacting with the Dynamic Language Runtime initially? Or more likely because Func goes up to arity 16 and I needed up to arity 20. You can find it https://github.com/clojure/clojure-clr/blob/master/Clojure/Clojure/Lib/AFnImpl.cs . If you also want & arg handling -- maybe don't. But I did that also, https://github.com/clojure/clojure-clr/blob/master/Clojure/Clojure/Lib/RestFnImpl.cs (This code dates back to my first commit to the repo in 2009. For some reason, I'm not feeling any nostalgia.)

taryn 2025-01-13T14:19:50.051049Z

Thank you both. This gives me multiple routes to go. I will see what works best for my current situation and move forward.