Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make it easier to declare external types #1054

Closed
alfonsogarciacaro opened this issue Aug 2, 2021 · 12 comments
Closed

Make it easier to declare external types #1054

alfonsogarciacaro opened this issue Aug 2, 2021 · 12 comments

Comments

@alfonsogarciacaro
Copy link

alfonsogarciacaro commented Aug 2, 2021

I propose we make it easier to declare external types. As F# is being used in more runtimes beyond .net we need a way to easily declare types from external languages. This is a paint point in Fable right now. Currently we use interfaces to avoid the limitations of F# classes and the ugly syntax with dummy implementations, but this requires us to declare the type in 3 parts (one interface for instance members, another for static members and a module value to access the static part) besides making it not possible to use type constructors.

I can think of two possible solutions but other suggestions are welcome:

  • Enable "empty" .fsi signature files (empty in the sense they don't need a corresponding .fs file) either with a special attribute or a pragma. Note the .fsi signatures should support attributes, which seems to be the case now.
  • Extend the use of extern keyword as in C# or add a mechanism to enable empty type declarations, like:
[<Extern>]
type Foo(x: int) =
    member _.Foo(x: string, y: float): int
    member _.Value: int
    static member AsBar(foo: Foo): Bar

Note the extern keyword works in F# now but apparently it's limited to module values and, very unexpectedly, uses C-like syntax! 😮

Please note this suggestions is NOT about altering the F# type system (e.g. adding structural typing) and it's not about Typescript/JS in particular. It's about enabling some syntax to declare types (classes) as they're currently possible in F# but without implementation. Calls to members of these types won't trigger compilations errors and will appear normally in the typed AST (when compiling to .NET IL dummy classes throwing runtime errors could be generated).

@OnurGumus
Copy link

I think .fsi approach would be idiomatic and won't have any unwanted effects.

@0x53A
Copy link
Contributor

0x53A commented Aug 2, 2021

This could also play together with Reference Assemblies.

A suggested spec could be:

  • when using FCS, external types are fully represented with all their information
  • when compiling a normal assembly using fsc.exe, it fails when encountering an external type
  • when compiling a reference assembly using fsc.exe, it succeeds, does emit the external type (and obviously does not emit method bodies)

@Tarmil
Copy link

Tarmil commented Aug 2, 2021

I haven't done that much interop in Fable, but WebSharper does this by providing the following in its standard library:

type StubAttribute() = inherit Attribute()

exception ClientSideOnly
let X<'T> = raise ClientSideOnly

The WS compiler recognizes and processes StubAttribute specially to ignore the method bodies and emit external calls instead. So external libraries can be defined like this:

[<Stub>]
type Foo(x: int) =
    member _.Foo(x: string, y: float) = X<int>
    member _.Value = X<int>
    static member AsBar(foo: Foo) = X<Bar>

I think a standardized "extern" syntax would add one main advantage, which is that the stock F# compiler would be able to detect and reject external calls, whereas .NET calls to X<'T> only raise runtime errors.

@charlesroddie
Copy link

Currently we use interfaces to avoid the limitations of F# classes and the ugly syntax with dummy implementations, but this requires us to declare the type in 3 parts (one interface for instance members, another for static members and a module value to access the static part)

Will static interface methods fix this problem?

@dsyme
Copy link
Collaborator

dsyme commented Aug 6, 2021

Some technical questions

  1. I roughly understand what you intend to happen with FCS - it would report a call to the construct (and no corresponding implementation code for it). However which assembly would the construct be considered to be in from the point of view of the symbol definition, type equality and so on?

  2. What should the F# .NET backend do when it tries to emit code for such a construct? Just fail? So this construct would be for FCS-based compilers to other targets only?

  3. Given @Tarmil's comment would a single Extern or Stub attribute in FSharp.Core suffice? But why not do tha

Currently we use interfaces to avoid the limitations of F# classes and the ugly syntax with dummy implementations..

Can you explain more why a solution like StubAttribute mentioned by @Tarmil doesn't work? What would extern declarations give you over this?

when compiling a normal assembly using fsc.exe, at the Emit phase, external types are ignored and it fails with a hard error at the usage point

Why wouldn't we fail at the definition point when emitting a normal .NET assembly? There's no point in declaring such definitions when emitting a normal .NET assembly?

@0x53A
Copy link
Contributor

0x53A commented Aug 6, 2021

Why wouldn't we fail at the definition point when emitting a normal .NET assembly? There's no point in declaring such definitions when emitting a normal .NET assembly?

You are right, it probably doesn't make sense. I was thinking about the case where you might want to use a single fsproj in both .net and fable, and how to make that as seamless as possible, but I guess then the external types would need to be #ifed out, since they can't be used anyway.

@alfonsogarciacaro
Copy link
Author

Sorry for the late reply!

  1. Which assembly would the construct be considered to be in from the point of view of the symbol definition, type equality and so on?

I'm assuming in the assembly to which the source file including the declaration belongs. In the case of Fable, we usually decorate these types with an attribute Import/Global to say where's the actual JS type, so it doesn't really matter.

  1. What should the F# .NET backend do when it tries to emit code for such a construct? Just fail? So this construct would be for FCS-based compilers to other targets only? Why wouldn't we fail at the definition point when emitting a normal .NET assembly? There's no point in declaring such definitions when emitting a normal .NET assembly?

Yes, I'm mainly thinking in F# compilers targeting other platforms than .NET (although maybe this could also be used to declare external code from C++/Java/Swift when using .NET mobile?) I guess it's ok to fail immediately when trying to build for .NET, although I know of some devs that use dotnet build in their Fable projects pipelines to make sure the project contains no errors.

  1. Can you explain more why a solution like StubAttribute mentioned by @Tarmil doesn't work? What would extern declarations give you over this?

Sorry I was not clear enough. @Tarmil solution does indeed work and we already do something similar with the Import/Global attributes. My main concern is that the syntax to declare an interface with abstract signatures and a class with concrete methods is very different. In general we use interfaces for the bindings (as ts2fable does) but if you discover you need to use a class instead (because now you need to call a static member) then you have to change * to ,, some -> to : and lots of braces (besides adding the dummy implementations) which is quite unintuitive, particularly for beginners.

But yes, maybe we should just recommend to use classes with dummy implementations from now on and provide a script or an IDE tool to automatically convert current bindings. There are already some discussions about this in the Fable repo 😅

@0x53A Didn't know about the reference assemblies proposal, thanks for pointing that out! I think this could be beneficial for assemblies containing only bindings as with the Fable.Browser.* packages. although I would still like to have a way to easily declare bindings directly in source code. Not sure what would be the speed gain when loading these assemblies if we can skip the implementation data. But this is actually important for the REPL which does download assemblies and we try to reduce their size as much as possible. In fact @ncave has a branch of the F# compiler that (I guess) does basically that: emit assemblies by keeping only the metadata, and we use it to build the F# assemblies we have in the REPL.

Will static interface methods fix this problem?

@charlesroddie I would need to check the static interface methods proposal, but I think it would probably not be enough because you still need an implementing type to know the location of the static method.

@alfonsogarciacaro
Copy link
Author

Another issue that obfuscates the syntax of empty classes is the fact that virtual methods are quite verbose. I believe that this is to discourage the use of virtual methods, but in JS you could say methods are virtual by default, and overriding them is a common pattern (for example when declaring web components), so most of the times we would have to duplicate all methods
(abstract and default declaration with the dummy implementation) just in case.

open Fable.Core

[<ImportMember(from="my-pkg")>]
type Parent() =
    member _.Bar: string = jsNative

    abstract Foo: string -> string
    default _.Foo(x: string): string = jsNative

type Child() =
    inherit Parent()
    override this.Foo(x) = base.Foo(x) + this.Bar

Maybe allowing the empty classes (without implementation) just for bindings would allow us to enable virtual without fear devs use it more in "normal" classes.

@dsyme
Copy link
Collaborator

dsyme commented Jun 14, 2022

Just to say I can't see a concrete suggestion here that isn't sufficiently covered by using an attribute and a class with dummy implementations. The dummy implementations aren't so hard to write or generate, and saves us needing a way to specify what code gets generated if compiled as .NET code.

I'll mark this as "probably not" for these reasons. (However the enthusiastic number of votes makes it feel like there's some need here not being met, so I'm aware we may want to revisit this)

@alfonsogarciacaro
Copy link
Author

You're right @dsyme. After writing bindings for the new compilation targets in Fable 4 I can do most of the things with dummy implementations so probably it's not worth to add more compiler machinery just for this. I will close the suggestion. Actually the main problem I'm having right now is the lack of support of optional arguments in module functions. Because in languages like Python/JS/Dart modules/files can contain both classes and functions with optional arguments, I need to have a separate module (to host the classes) and a class (to host the functions with optional arguments as static members), which makes it complicated to simulate the structure of the imported library. It does work if I always qualify the module/class because F# inserts the -Module suffix automatically but not if I want to use AutoOpen. Related discussion: fable-compiler/Fable#2922

I think I'm to blame for the upvotes because at some point I asked people to upvote this to improve Fable bindings in Twitter 😅

@dsyme dsyme reopened this Jun 15, 2022
@dsyme dsyme closed this as not planned Won't fix, can't repro, duplicate, stale Jun 15, 2022
@dsyme
Copy link
Collaborator

dsyme commented Jun 15, 2022

Thanks @alfonsogarciacaro

Could you generate static members and use AutoOpen on the class?

@alfonsogarciacaro
Copy link
Author

@dsyme Oh my! For some reason, I believed that AutoOpen couldn't be used with classes 🤦 This works, thank you!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

6 participants