fslang-suggestions
fslang-suggestions copied to clipboard
Add callables/invokables
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.
Will it be allowed to add an extension Invoke
method for a given type?
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?
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.
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
)?
What about interactions with currying vs tuples?
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.
As an example in Python, note the use of model(features, obj)
on line 5:
Also this kind of code:
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 attributeDefaultInvokeAttribute
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 theGetSlice
method - We'd have to consider whether an object is allowed to support both indexing and application - I don't see why not.
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'
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
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.
Or the other way: can we attach properties to functions? This can be done by storing all functions in a map.
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
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 ?
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.
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
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
Weren't we going to disallow a[0]
as invoking? A space must be added.
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.