fsharp icon indicating copy to clipboard operation
fsharp copied to clipboard

C# interface can't be implemented on F#

Open josephmoresena opened this issue 1 year ago • 9 comments

I am trying to port from C# to F# the jnetinterface branch of the NativeAOT-AndroidHelloJniLib repository, but when attempting to use the basic AndroidToast object, I am encountering 1 design-time error and several runtime errors.

AndroidContext.cs

using Rxmxnx.JNetInterface.Native;
using Rxmxnx.JNetInterface.Types;
using Rxmxnx.JNetInterface.Types.Metadata;

namespace HelloJniLib;

public class AndroidContext : JLocalObject, IClassType<AndroidContext>
{
    private static readonly JClassTypeMetadata<AndroidContext> typeMetadata = TypeMetadataBuilder<AndroidContext>
                                                                              .Create("android/content/Context"u8,
                                                                                  JTypeModifier.Abstract).Build();

    static JClassTypeMetadata<AndroidContext> IClassType<AndroidContext>.Metadata => AndroidContext.typeMetadata;

    protected AndroidContext(IReferenceType.ClassInitializer initializer) : base(initializer) { }
    protected AndroidContext(IReferenceType.GlobalInitializer initializer) : base(initializer) { }
    protected AndroidContext(IReferenceType.ObjectInitializer initializer) : base(initializer) { }

    static AndroidContext IClassType<AndroidContext>.Create(IReferenceType.ClassInitializer initializer)
        => new(initializer);
    static AndroidContext IClassType<AndroidContext>.Create(IReferenceType.ObjectInitializer initializer)
        => new(initializer);
    static AndroidContext IClassType<AndroidContext>.Create(IReferenceType.GlobalInitializer initializer)
        => new(initializer);
}

AndroidContext.fs

namespace HelloJniLib

open Rxmxnx.JNetInterface.Native
open Rxmxnx.JNetInterface.Types

type AndroidContext =
    inherit JLocalObject

    static let typeMetadata = JLocalObject.TypeMetadataBuilder<AndroidContext>.Create("android/content/Context"B).Build()

    static member val Metadata = typeMetadata with get

    new (initializer: IReferenceType.ClassInitializer) = { inherit JLocalObject(initializer); }
    new (initializer: IReferenceType.GlobalInitializer) = { inherit JLocalObject(initializer) }
    new (initializer: IReferenceType.ObjectInitializer) = { inherit JLocalObject(initializer) }

    interface IClassType<AndroidContext> with
        static member get_Metadata() = AndroidContext.Metadata
        static member Create(initializer: IReferenceType.ClassInitializer) = new AndroidContext(initializer)
        static member Create(initializer: IReferenceType.GlobalInitializer) = new AndroidContext(initializer)
        static member Create(initializer: IReferenceType.ObjectInitializer) = new AndroidContext(initializer)

Disign error: The override 'Create: IReferenceType.ObjectInitializer -> AndroidContext' implements more than one abstract slot, e.g. 'IClassType.Create(initializer: IReferenceType.ObjectInitializer) : AndroidContext' and 'IReferenceType.Create(initializer: IReferenceType.ObjectInitializer) : AndroidContext'

Compilation error:

AndroidContext.fs(17,15): Error FS0366 : No implementation was given for those members: 
 'IClassType.Create(initializer: IReferenceType.ClassInitializer) : AndroidContext'
 'IClassType.Create(initializer: IReferenceType.ObjectInitializer) : AndroidContext'
 'IClassType.Create(initializer: IReferenceType.GlobalInitializer) : AndroidContext'
 'IReferenceType.Create(initializer: IReferenceType.ObjectInitializer) : AndroidContext'
 'IDataType.get_Metadata() : Metadata.JDataTypeMetadata'
Note that all interface members must be implemented and listed under an appropriate 'interface' declaration, e.g. 'interface ... with member ...'.
1>------- Finished building project: AndroidHelloJniFSharp. Succeeded: False. Errors: 1. Warnings: 1

The interface I am trying to implement is a sub-interface that internally implements static methods/properties from its super interfaces with new methods with same signature.

public interface IDataType
{
    internal static abstract JTypeKind Kind { get; }
    internal static abstract Type? FamilyType { get; }

    public static JDataTypeMetadata GetMetadata<TDataType>() where TDataType : IDataType<TDataType>
        => TDataType.Metadata;
    public static String GetHash<TDataType>() where TDataType : IDataType<TDataType>
        => IDataType.GetMetadata<TDataType>().Hash;
}

public partial interface IReferenceType : IObject, IDataType, IDisposable
{
    static Type IDataType.FamilyType => typeof(JLocalObject);

    public new static JReferenceTypeMetadata
        GetMetadata<TReference>()
        where TReference : JReferenceObject, IReferenceType<TReference>
        => (JReferenceTypeMetadata)IDataType.GetMetadata<TReference>();
}

public interface IClassType : IReferenceType
{
    static JTypeKind IDataType.Kind => JTypeKind.Class;

    public new static JClassTypeMetadata GetMetadata<TClass>() where TClass : JLocalObject, IClassType<TClass>
        => (JClassTypeMetadata)IDataType.GetMetadata<TClass>();
}

public interface IDataType<out TDataType> : IDataType where TDataType : IDataType<TDataType>
{
    internal static virtual JArgumentMetadata Argument { get; } = JArgumentMetadata.Create<TDataType>();
    internal static abstract JDataTypeMetadata Metadata { get; }
}

public interface IReferenceType<out TReference> : IReferenceType, IDataType<TReference> where TReference : JReferenceObject, IReferenceType<TReference>
{
    internal static IReadOnlySet<Type> TypeInterfaces => TypeInterfaceHelper<TReference>.TypeInterfaces;

    protected static abstract TReference Create(ObjectInitializer initializer);
}

public interface IClassType<TClass> : IClassType, IReferenceType<TClass> where TClass : JLocalObject, IClassType<TClass>
{
    internal static IReadOnlySet<Type> TypeBaseTypes => BaseTypeHelper<TClass>.TypeBaseTypes;

    protected new static abstract JClassTypeMetadata<TClass> Metadata { get; }

    static JDataTypeMetadata IDataType<TClass>.Metadata => TClass.Metadata;

    protected static abstract TClass Create(ClassInitializer initializer);
    protected new static abstract TClass Create(ObjectInitializer initializer);
    protected static abstract TClass Create(GlobalInitializer initializer);

    static TClass IReferenceType<TClass>.Create(ObjectInitializer initializer) => TClass.Create(initializer);
}
  • Operating system : macOS Montery
  • .NET 8.0

josephmoresena avatar Aug 26 '24 02:08 josephmoresena

In F# we usually require explicit interfaces implementations (hence the diagnostic about IClassType). Not sure if it's on purpose or not in statics, need to make a minimal repro and see how it behaves in the instance ones, the hierarchy is quite bloated, hard to see what's going on here without some investigation.

vzarytovskii avatar Aug 26 '24 07:08 vzarytovskii

In F# we usually require explicit interfaces implementations (hence the diagnostic about IClassType). Not sure if it's on purpose or not in statics, need to make a minimal repro and see how it behaves in the instance ones, the hierarchy is quite bloated, hard to see what's going on here without some investigation.

It was possible to reduce errors by explicitly implementing the create method from the IReferenceType<> superinterface. As for the Metadata property of the IDataType<> interface, it was impossible for me to do so because it is internal.

The purpose of it being internal is to ensure that it could only be implemented by a own generic type, even though the property itself is not generic. The intention behind the hierarchy is to emulate Java's data types, in this case:
DataType -> ReferenceType -> Class.

There are also these types:
DataType -> PrimitiveType
DataType -> ReferenceType -> Interface
DataType -> ReferenceType -> Enum
DataType -> ReferenceType -> Class -> PrimitiveWrapper
DataType -> ReferenceType -> Class -> Throwable

All interfaces in the hierarchy have static methods that facilitate the use of types that implement them with JNI from .NET.

josephmoresena avatar Aug 26 '24 11:08 josephmoresena

In F# we usually require explicit interfaces implementations (hence the diagnostic about IClassType). Not sure if it's on purpose or not in statics, need to make a minimal repro and see how it behaves in the instance ones, the hierarchy is quite bloated, hard to see what's going on here without some investigation.

It was possible to reduce errors by explicitly implementing the create method from the IReferenceType<> superinterface. As for the Metadata property of the IDataType<> interface, it was impossible for me to do so because it is internal.

The purpose of it being internal is to ensure that it could only be implemented by a own generic type, even though the property itself is not generic. The intention behind the hierarchy is to emulate Java's data types, in this case: DataType -> ReferenceType -> Class.

There are also these types: DataType -> PrimitiveType DataType -> ReferenceType -> Interface DataType -> ReferenceType -> Enum DataType -> ReferenceType -> Class -> PrimitiveWrapper DataType -> ReferenceType -> Class -> Throwable

All interfaces in the hierarchy have static methods that facilitate the use of types that implement them with JNI from .NET.

I see, let me try and create a synthetic simple example which would show it with one method and one property and post it here, so we ca figure out whether this needs a language suggestion or a fix.

vzarytovskii avatar Aug 26 '24 11:08 vzarytovskii

So, after some testing, it looks like we expect it, since it behaves the same way with instance members:

image

Here's there repo with minimal repro: https://github.com/vzarytovskii/dotnet-fsharp-issues-17605

This will require a language suggestion, since I assume it was decided long time ago, and not an easy task to resolve cases like - "what implementation do we chose in case of "diamond" interface implementation", etc.

cc @dsyme

vzarytovskii avatar Aug 26 '24 12:08 vzarytovskii

@vzarytovskii perhaps discuss this with @dsyme next time you get a chance

abonie avatar Aug 26 '24 16:08 abonie

We technically can filter out inaccessible slots and check if it's a "new" slot that shadows them, and allow it in this case, but that would be a drastic change of what we allow.

vzarytovskii avatar Aug 26 '24 17:08 vzarytovskii

We technically can filter out inaccessible slots and check if it's a "new" slot that shadows them, and allow it in this case, but that would be a drastic change of what we allow.

This was a pain while creating Swift, Kotlin binding libraries in my Xamarin times. I think it makes sense to enable this to make F# more interoperable. I even would offer to help if there is a suggestion approved.

edgarfgp avatar Aug 27 '24 07:08 edgarfgp

We technically can filter out inaccessible slots and check if it's a "new" slot that shadows them, and allow it in this case, but that would be a drastic change of what we allow.

This was a pain while creating Swift, Kotlin binding libraries in my Xamarin times. I think it makes sense to enable this to make F# more interoperable. I even would offer to help if there is a suggestion approved.

It will be changing something what has been decided long time ago. This will need a suggestion and an explicit approval from @dsyme.

vzarytovskii avatar Aug 27 '24 08:08 vzarytovskii

We technically can filter out inaccessible slots and check if it's a "new" slot that shadows them, and allow it in this case, but that would be a drastic change of what we allow.

Note that in this case, not only is the property being hidden by the sub-interface, but theoretically, it is being virtually implemented by the one that is publicly exposed.

protected new static abstract JClassTypeMetadata<TClass> Metadata { get; }

static JDataTypeMetadata IDataType<TClass>.Metadata => TClass.Metadata;

josephmoresena avatar Aug 27 '24 11:08 josephmoresena

Given Vlad's comments above, this is more of a feature request than a bug. I am going to close this, but feel free to create a language suggestion

abonie avatar Sep 09 '24 16:09 abonie