fslang-suggestions icon indicating copy to clipboard operation
fslang-suggestions copied to clipboard

Add callables/invokables

Open dsyme opened this issue 3 years ago • 18 comments

In discussion with @KathleenDollard the topic of callable/invokable objects came up. This isn't really advocating the suggestion, rather just creating a tracking issue for it with notes.

This exists in other languages but surprisingly this hasn't been requested for F# AFAICS so I thought I'd make some notes - it's not a totally terrible idea and is natural in machine learning programming and elsewhere.

For example a method called Invoke (possibly with an attribute) may make an object callable/invokable:

    type Adder(c:int) = 
        member _.Invoke v = v + c

    let adder = Adder(3)
    adder 4 // short for adder.Invoke(4)

Now, if you can do the above then all of the following should really work too:

    let adder1 = Adder 3
    let adder2 = Adder 4

    adder1(3)
    adder 3
    3 |> adder
    List.map (adder1 >> adder2)
    adder1 >> adder2

and be equivalent to:

    //Callables:
    let _ = adder1.Invoke(3)
    let _ = adder1.Invoke 3
    let _ = 3 |> (fun s -> adder1.Invoke(s))
    let _ = List.map (adder1 >> adder2)
    let _ = (fun s -> adder1.Invoke(s)) >> (fun s -> adder2.Invoke(s)) // Or would this make a new Adder?

Note the question in the last - would we go as far as altering f >> g to allow combinations of delegates, functions and callables, and if we did would the composition still always be a function?

Also note that delegates should be considered callables so func.Invoke(arg) can be func arg.

Pros and Cons

The advantages of making this adjustment to F# are it allows for some nice machine learning model DSLs

The disadvantages of making this adjustment to F# are it adds subtlety and gives another rope to hang yourself on.

Extra information

Estimated cost (XS, S, M, L, XL, XXL): L

Related suggestions: (put links to related suggestions here)

Affidavit (please submit!)

Please tick this by placing a cross in the box:

  • [x] This is not a question (e.g. like one you might ask on stackoverflow) and I have searched stackoverflow for discussions of this issue
  • [x] I have searched both open and closed suggestions on this site and believe this is not a duplicate
  • [x] This is not something which has obviously "already been decided" in previous versions of F#. If you're questioning a fundamental design decision that has obviously already been taken (e.g. "Make F# untyped") then please don't submit it.

Please tick all that apply:

  • [x] This is not a breaking change to the F# language design
  • [x] I or my company would be willing to help implement and/or test this

For Readers

If you would like to see this issue implemented, please click the :+1: emoji on this issue. These counts are used to generally order the suggestions by engagement.

dsyme avatar Oct 11 '21 16:10 dsyme

Will it be allowed to add an extension Invoke method for a given type?

vzarytovskii avatar Oct 11 '21 16:10 vzarytovskii

The disadvantages of making this adjustment to F# are it adds subtlety and gives another rope to hang yourself on.

Yeah, although it may have very interesting applications, the idea of "callable instances" (which may return callable instances, and so on) may be very confusing (i.e. code may be confusing to read and work with).

Also, a good question is how to reflect type information on such instances in tooling?

vzarytovskii avatar Oct 11 '21 16:10 vzarytovskii

it does sort of lead naturally into a Callable constraint (which may or may not be structured as a typeclass/static interface of some kind), and then the type of |> would expand to allow the right hand side to be an instance of Callable<'t> matching the 't on the left. This kind of constraint is a whole other box of worms as we're all aware.

baronfel avatar Oct 11 '21 17:10 baronfel

Also, a question: how will it work with SRTPs? Will it require specifying concrete member requirement (i.e. for member Invoke, and then calling it using SRTP syntax), or some special one? In case of latter, should it have its own constraint, like we have with comparison/equality/enum/delegate (like 'T when 'T : callable<int, int>, or just re-use delegate)?

vzarytovskii avatar Oct 11 '21 17:10 vzarytovskii

What about interactions with currying vs tuples?

Happypig375 avatar Oct 11 '21 17:10 Happypig375

it does sort of lead naturally into a Callable constraint Also, a question: how will it work with SRTPs

No change, you'd use an existing SRTP constraint for an Invoke method as a constraint. We might need a tweak so that functions should be considered to satisfy such a constraint

What about interactions with currying vs tuples?

It would be one argument expression f argexpr and resolve exactly like f.Invoke argexpr so f (x1,x2) allowed. If the thing wants to return a function or another callable that's fine.

dsyme avatar Oct 11 '21 17:10 dsyme

As an example in Python, note the use of model(features, obj) on line 5:

image

Also this kind of code:

image

can become

    override _.forward(input) =
        input |> layer1 |> layer2 |> layer3 |> layer4 |> layer5 |> flatten |> dense1 |> dense2 |> output

When Python-familiar people view the F# equivalent code one of the first things people ask is "can we get rid of the .forward"?

Notes:

  • Here the Invoke is called forward and we may prefer to allow an attribute DefaultInvokeAttribute or similar to label the method. This is like you can do with indexer notation with DefaultMemberAttribute.
  • DefaultMemberAttribute is a little oddly named. From the F# perspective identifies the indexing member f[x].
  • For consistency we could have DefaultSlicingAttribute to identify altenative names for the GetSlice method
  • We'd have to consider whether an object is allowed to support both indexing and application - I don't see why not.

dsyme avatar Oct 11 '21 18:10 dsyme

This is an interesting extension to the F# object model that I find interesting. I like it a bunch and think it could lead to some pretty nice patterns.

@vzarytovskii re this:

Will it be allowed to add an extension Invoke method for a given type?

I would expect so, yeah, since you can do this with other special methods today (CE methods, index, getslice, etc.)

So in theory you should be able to do bullshit like this:

type System.String with
    member _.Invoke s = $"yeet: {s}"

let yeet = "uhh"
yeet "vlad" // 'yeet: vlad'

cartermp avatar Oct 11 '21 18:10 cartermp

Can we invoke modules?

module Domain =
    type UnvalidatedEmail = UnvalidatedEmail of string
    module UnvalidatedEmail =
        let empty = UnvalidatedEmail ""
    type Email = private Email of string
    module Email =
        // https://stackoverflow.com/a/201378/5429648
        let private r = System.Text.RegularExpressions.Regex """(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\.){3}(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9])|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])"""
        let parse (UnvalidatedEmail x) = if r.IsMatch x then Some (Email x) else None
        let Invoke = parse
    let (|Email|) (Email x) = x
open Domain
let a = UnvalidatedEmail "hi"
let (UnvalidatedEmail b) = a
let c = Email a
let (Email d) = c |> Option.get

Happypig375 avatar Oct 13 '21 08:10 Happypig375

The interest here is in abstracting from FSharpFunc, System.Func, and other function-like objects.

Why do this by looking for an Invoke method? Why not have an interface (coordinated with dotnet/runtime for System.Func) IFunc<'a,'b>?

If there were such an interface, and also an interface IUnit, the a lot of dotnet function classes might be consolidated without backwards compatibility problems.

charlesroddie avatar Oct 17 '21 12:10 charlesroddie

Or the other way: can we attach properties to functions? This can be done by storing all functions in a map.

yatli avatar Oct 17 '21 12:10 yatli

Why do this by looking for an Invoke method? Why not have an interface (coordinated with dotnet/runtime for System.Func) IFunc<'a,'b>?

Partly because it's a much, much longer lead-time for this change, requiring coordination with C# and .NET. And also it doesn't really fit with .NET (e.g. we don't do that for indexing or slicing). Also it doesn't really support overloading the Invoke method, which C# would surely want for parity with Item property getter

dsyme avatar Oct 17 '21 12:10 dsyme

Actually when I thought about workarounding the limitation of Higher Ranks by passing a type instead of a function, the first thing that came to my mind is to have a mechanism like this in place.

What about having a static member Invoke ?

gusty avatar Oct 19 '21 20:10 gusty

Are we planning on adding support for equality in Invocables? I've seen at least one use case (basically an event handler) that would benefit from being able to add/remove an Invocable from a data structure, and to do that at the very least reference equality is required.

JustinWick avatar Oct 20 '21 08:10 JustinWick

Will invokables be assignable as lambdas?

let f (g: string -> unit) = g "foo"
type ConsolePrinter() =
    member _.Invoke s = System.Console.WriteLine s
let cons = ConsolePrinter()
f cons // prints "foo"
// workaround if that won't be allowed; feels a bit silly
f (fun s -> cons s)

Will they be curryable?

type Adder() =
    member _.Invoke x y = x + y
let add = Adder()
let inc = add 1
inc 4 // 5

if not; manually currying invokables should be possible:

type Adder1(x:int) =
    member _.Invoke y = x + y
type Adder() =
    member _.Invoke x = Adder1(x)
let add = Adder()
add 1 4

jwosty avatar Oct 20 '21 16:10 jwosty

We'd have to consider whether an object is allowed to support both indexing and application

Given this class

type MyClass() =
    member _.Item (index: int) = 0
    member _.Invoke (arg: int list) = 1

which method will be invoked?

let inst = MyClass()
inst[0] |> printfn "%d"

It can be indexing or invoking with list. If language is going to prefer indexing, then it may result in unpredictable behavior:

inst[] // Invoke
inst[0] // indexer
inst[0;1] // Invoke

// if type have 2 element indexer: _.Item (index1: int, index2: int)
inst[0,1] // indexer
inst[0,1;2,3] // Invoke

jl0pd avatar Nov 09 '21 03:11 jl0pd

Weren't we going to disallow a[0] as invoking? A space must be added.

Happypig375 avatar Nov 09 '21 03:11 Happypig375

Weren't we going to disallow a[0] as invoking? A space must be added.

No, but it's an info-level warning:

inst[0;1] // info FS3365: The syntax 'expr1[expr2]' is used for indexing.

So I don't think it will be terribly surprising since the "surprising" behavior gets your editor and compiler yelling at you until you had a space, at which point it's clear what's going on.

cartermp avatar Nov 09 '21 05:11 cartermp