F # 및 C # 모두에서 사용할 F # 라이브러리를 디자인하는 가장 좋은 방법


113

F #으로 라이브러리를 디자인하려고합니다. 라이브러리는 F # 및 C # 모두 에서 사용하기에 친숙해야합니다 .

그리고 이것은 제가 약간 갇혀있는 곳입니다. F # 친화적으로 만들거나 C # 친화적으로 만들 수 있지만 문제는 둘 다 친화적으로 만드는 방법입니다.

여기에 예가 있습니다. F #에 다음과 같은 기능이 있다고 상상해보십시오.

let compose (f: 'T -> 'TResult) (a : 'TResult -> unit) = f >> a

이것은 F #에서 완벽하게 사용할 수 있습니다.

let useComposeInFsharp() =
    let composite = compose (fun item -> item.ToString) (fun item -> printfn "%A" item)
    composite "foo"
    composite "bar"

C #에서 compose함수에는 다음과 같은 서명이 있습니다.

FSharpFunc<T, Unit> compose<T, TResult>(FSharpFunc<T, TResult> f, FSharpFunc<TResult, Unit> a);

하지만 물론, 나는 FSharpFunc서명을 원하지 않습니다. 내가 원하는 것은 다음 FuncAction같습니다.

Action<T> compose2<T, TResult>(Func<T, TResult> f, Action<TResult> a);

이를 위해 다음 compose2과 같은 함수 를 만들 수 있습니다 .

let compose2 (f: Func<'T, 'TResult>) (a : Action<'TResult> ) = 
    new Action<'T>(f.Invoke >> a.Invoke)

이제 이것은 C #에서 완벽하게 사용할 수 있습니다.

void UseCompose2FromCs()
{
    compose2((string s) => s.ToUpper(), Console.WriteLine);
}

하지만 이제 compose2F #에서 사용하는 데 문제가 있습니다 ! 이제 funs다음 FuncAction같이 모든 표준 F # 을 and 로 래핑해야 합니다.

let useCompose2InFsharp() =
    let f = Func<_,_>(fun item -> item.ToString())
    let a = Action<_>(fun item -> printfn "%A" item)
    let composite2 = compose2 f a

    composite2.Invoke "foo"
    composite2.Invoke "bar"

질문 : F # 및 C # 사용자 모두를 위해 F #으로 작성된 라이브러리의 최고 수준의 경험을 얻으려면 어떻게해야합니까?

지금까지이 두 가지 접근 방식보다 더 나은 방법을 찾을 수 없었습니다.

  1. 두 개의 개별 어셈블리 : 하나는 F # 사용자를 대상으로하고 다른 하나는 C # 사용자를 대상으로합니다.
  2. 하나의 어셈블리이지만 다른 네임 스페이스 : 하나는 F # 사용자 용이고 다른 하나는 C # 사용자 용입니다.

첫 번째 접근 방식의 경우 다음과 같이합니다.

  1. F # 프로젝트를 만들고 FooBarFs라고 부르고 FooBarFs.dll로 컴파일합니다.

    • 순수하게 F # 사용자에게만 라이브러리를 대상으로 지정합니다.
    • .fsi 파일에서 불필요한 모든 것을 숨 깁니다.
  2. 다른 F # 프로젝트를 만들고 FooBarCs를 호출 한 다음 FooFar.dll로 컴파일합니다.

    • 소스 수준에서 첫 번째 F # 프로젝트를 다시 사용합니다.
    • 해당 프로젝트에서 모든 것을 숨기는 .fsi 파일을 만듭니다.
    • 이름, 네임 스페이스 등에 C # 관용구를 사용하여 C # 방식으로 라이브러리를 노출하는 .fsi 파일을 만듭니다.
    • 필요한 경우 변환을 수행하여 코어 라이브러리에 위임하는 래퍼를 만듭니다.

네임 스페이스를 사용한 두 번째 접근 방식은 사용자에게 혼동을 줄 수 있지만 어셈블리가 하나 있습니다.

질문 : 이것들 중 어느 것도 이상적이지 않습니다. 아마도 어떤 종류의 컴파일러 플래그 / 스위치 / 속성 또는 어떤 종류의 트릭을 놓치고 있으며 더 나은 방법이 있습니까?

질문 : 다른 사람이 비슷한 것을 달성하려고 시도한 적이 있습니까? 그렇다면 어떻게 하셨나요?

편집 : 명확히하기 위해 질문은 함수 및 대리자뿐만 아니라 F # 라이브러리를 사용하는 C # 사용자의 전반적인 경험에 관한 것입니다. 여기에는 C # 고유의 네임 스페이스, 명명 규칙, 관용구 등이 포함됩니다. 기본적으로 C # 사용자는 라이브러리가 F #으로 작성되었음을 감지 할 수 없습니다. 그 반대의 경우도 F # 사용자는 C # 라이브러리를 다루는 것처럼 느껴야합니다.


편집 2 :

지금까지 답변과 의견에서 내 질문에 필요한 깊이가 부족하다는 것을 알 수 있습니다. 아마도 F #과 C # 간의 상호 운용성 문제가 발생하는 하나의 예인 함수 값 문제를 사용했기 때문일 것입니다. 나는 이것이 가장 명백한 예라고 생각하고 그래서 나는 그것을 사용하여 질문을하게되었지만, 같은 토큰으로 이것이 내가 관심있는 유일한 문제라는 인상을주었습니다.

좀 더 구체적인 예를 들어 보겠습니다. 가장 뛰어난 F # 구성 요소 디자인 지침 문서를 읽었습니다 (이에 대해 @gradbot에게 감사드립니다!). 문서의 지침이 사용되는 경우 일부 문제를 다루지 만 전부는 아닙니다.

이 문서는 두 가지 주요 부분으로 나뉩니다. 1) F # 사용자를 대상으로하는 지침; 2) C # 사용자를 대상으로하는 지침. 어느 곳에서도 균일 한 접근 방식이 가능한 척 시도하지 않습니다. 내 질문을 정확히 반영합니다. F #을 대상으로 할 수 있고 C #을 대상으로 할 수 있지만 둘 다를 대상으로하는 실용적인 솔루션은 무엇입니까?

다시 말해, 목표는 F #으로 작성된 라이브러리를 보유하고 F # 및 C # 언어 모두에서 관용적 으로 사용할 수 있도록하는 것 입니다.

여기서 키워드는 관용적 입니다. 문제는 다른 언어로 라이브러리를 사용할 수있는 일반적인 상호 운용성이 아닙니다.

이제 F # Component Design Guidelines 에서 직접 가져온 예제로 이동합니다 .

  1. 모듈 + 함수 (F #) 대 네임 스페이스 + 유형 + 함수

    • F # : 네임 스페이스 또는 모듈을 사용하여 형식과 모듈을 포함하세요. 관용적으로 사용하는 것은 모듈에 함수를 배치하는 것입니다. 예 :

      // library
      module Foo
      let bar() = ...
      let zoo() = ...
      
      
      // Use from F#
      open Foo
      bar()
      zoo()
    • C # : 바닐라 .NET API의 경우 네임 스페이스, 형식 및 멤버를 구성 요소의 기본 조직 구조로 사용하십시오 (모듈이 아닌).

      이것은 F # 지침과 호환되지 않으며 C # 사용자에 맞게 예제를 다시 작성해야합니다.

      [<AbstractClass; Sealed>]
      type Foo =
          static member bar() = ...
          static member zoo() = ...

      그렇게 생각함으로써, 우리는 F 번호의 관용적 인 사용을 중단 우리가 더 이상 사용을 할 수 있기 때문에 barzoo를 앞에없이 Foo.

  2. 튜플 사용

    • F # : 반환 값에 적절한 경우 튜플을 사용합니다.

    • C # : vanilla .NET API에서 튜플을 반환 값으로 사용하지 마십시오.

  3. 비동기

    • F # : F # API 경계에서 비동기 프로그래밍에 Async를 사용하세요.

    • C # : .NET 비동기 프로그래밍 모델 (BeginFoo, EndFoo)을 사용하거나 F # Async 개체가 아닌 .NET 작업 (Task)을 반환하는 메서드를 사용하여 비동기 작업을 노출합니다.

  4. 사용 Option

    • F # : 예외를 발생시키는 대신 반환 형식에 옵션 값을 사용하는 것이 좋습니다 (F # 관련 코드의 경우).

    • 바닐라 .NET API에서 F # 옵션 값 (옵션)을 반환하는 대신 TryGetValue 패턴을 사용하고 F # 옵션 값을 인수로 사용하는 것보다 메서드 오버로드를 선호합니다.

  5. 차별적 노조

    • F # : 트리 구조 데이터를 만드는 데 클래스 계층 구조 대신 구분 된 공용체를 사용하세요.

    • C # : 이에 대한 구체적인 지침은 없지만 차별 된 공용체의 개념은 C #과 는 다릅니다.

  6. 카레 기능

    • F # : 커리 함수는 F #의 관용적입니다.

    • C # : 바닐라 .NET API에서 매개 변수 커링을 사용하지 마십시오.

  7. null 값 확인

    • F # : 이것은 F #의 관용적이지 않습니다.

    • C # : 바닐라 .NET API 경계에서 null 값을 확인하는 것이 좋습니다.

  8. F 번호 유형의 사용 list, map, set, 등

    • F # : F # 에서 사용하는 것은 관용적입니다.

    • C # : 바닐라 .NET API의 매개 변수 및 반환 값에 .NET 컬렉션 인터페이스 유형 IEnumerable 및 IDictionary를 사용하는 것이 좋습니다. ( 즉, F 번호를 사용하지 않는 list, map,set )

  9. 함수 유형 (명백한 유형)

    • F # : F # 함수를 값으로 사용하는 것은 F #의 관용적입니다.

    • C # : vanilla .NET API의 F # 함수 유형보다 .NET 대리자 유형을 사용하세요.

나는 이것들이 내 질문의 본질을 입증하기에 충분하다고 생각합니다.

덧붙여, 지침에는 부분적인 답변이 있습니다.

... 바닐라 .NET 라이브러리에 대한 고차 메서드를 개발할 때 일반적인 구현 전략은 F # 함수 유형을 사용하여 모든 구현을 작성한 다음 실제 F # 구현 위에 얇은 파사드로 대리자를 사용하여 공용 API를 만드는 것입니다.

요약하자면.

한 가지 확실한 대답 이 있습니다 . 내가 놓친 컴파일러 트릭이 없습니다 .

지침 문서에 따르면 먼저 F # 용으로 저작 한 다음 .NET 용 파사드 래퍼를 만드는 것이 합리적인 전략 인 것 같습니다.

그러면 이것의 실제 구현에 관한 질문이 남아 있습니다.

  • 별도의 어셈블리? 또는

  • 다른 네임 스페이스?

내 해석이 맞다면, Tomas는 별도의 네임 스페이스를 사용하는 것으로 충분해야하며 수용 가능한 솔루션이어야한다고 제안합니다.

나는 네임 스페이스의 선택이 .NET / C # 사용자를 놀라게하거나 혼란스럽게하지 않는다는 점을 감안할 때 동의 할 것이라고 생각합니다. 즉, 네임 스페이스가 기본 네임 스페이스 인 것처럼 보일 것입니다. F # 사용자는 F # 관련 네임 스페이스를 선택하는 부담을 져야합니다. 예를 들면 :

  • FSharp.Foo.Bar-> 라이브러리를 향하는 F #의 네임 스페이스

  • Foo.Bar-> .NET 래퍼의 네임 스페이스, C #의 관용적



일류 함수를 전달하는 것 외에 어떤 문제가 있습니까? F #은 이미 다른 언어에서 원활한 경험을 제공하기 위해 먼 길을 가고 있습니다.
Daniel

Re : 귀하의 편집, F #으로 작성된 라이브러리가 C #에서 비표준으로 보이는 이유에 대해서는 아직 명확하지 않습니다. 대부분의 경우 F #은 C # 기능의 상위 집합을 제공합니다. 도서관을 자연스럽게 느끼게하는 것은 대부분 관습의 문제이며, 사용하는 언어에 관계없이 사실입니다. 즉, F #은이를 수행하는 데 필요한 모든 도구를 제공합니다.
Daniel

솔직히 코드 샘플을 포함하더라도 상당히 개방적인 질문을하고 있습니다. 당신은 그러나 당신이 줄 수있는 유일한 예는 정말 잘 지원되지 않는다는 뭔가 "는 F 번호 라이브러리는 C # 사용자의 전반적인 경험"에 대해 물어 어떤 필수 언어입니다. 함수를 일류 시민으로 취급하는 것은 함수형 프로그래밍의 핵심 원칙이며 대부분의 명령형 언어에 제대로 매핑되지 않습니다.
Onorio Catenacci

2
@KomradeP .: EDIT 2와 관련하여 FSharpx 는 상호 운용성을보다 쉽게 ​​만들 수있는 수단을 제공합니다. 이 훌륭한 블로그 게시물 에서 몇 가지 힌트를 찾을 수 있습니다.
패드

답변:


40

Daniel은 이미 작성한 F # 함수의 C # 친화적 버전을 정의하는 방법을 이미 설명 했으므로 몇 가지 더 높은 수준의 주석을 추가하겠습니다. 먼저 F # 구성 요소 디자인 지침 (gradbot에서 이미 참조)을 읽어야합니다. 이 문서는 F #을 사용하여 F # 및 .NET 라이브러리를 디자인하는 방법을 설명하는 문서이며 많은 질문에 답할 수 있습니다.

F #을 사용할 때 기본적으로 작성할 수있는 두 종류의 라이브러리가 있습니다.

  • F # 라이브러리F # 에서만 사용하도록 설계되었으므로 공용 인터페이스는 기능적 스타일로 작성됩니다 (F # 함수 유형, 튜플, 구별 된 공용체 등 사용).

  • .NET 라이브러리모든 .NET 언어 (C # 및 F # 포함) 에서 사용하도록 설계되었으며 일반적으로 .NET 개체 지향 스타일을 따릅니다. 즉, 대부분의 기능은 메서드 (때로는 확장 메서드 또는 정적 메서드가 있지만 대부분의 코드는 OO 디자인으로 작성되어야 함)가있는 클래스로 노출됩니다.

귀하의 질문에서 함수 구성을 .NET 라이브러리로 노출하는 방법을 묻고 있지만 귀하와 같은 함수 compose는 .NET 라이브러리 관점에서 너무 낮은 수준의 개념 이라고 생각합니다 . Func및 작업하는 메서드로 노출 할 수 있습니다.Action 있지만 처음에는 일반 .NET 라이브러리를 디자인하는 방법이 아닐 수 있습니다 (대신 Builder 패턴을 사용하거나 이와 유사한 것을 사용하는 경우).

경우에 따라 (예 : .NET 라이브러리 스타일에 잘 맞지 않는 숫자 라이브러리를 디자인 할 때) 단일 라이브러리에서 F #.NET 스타일을 모두 혼합하는 라이브러리를 디자인하는 것이 좋습니다 . 이를 수행하는 가장 좋은 방법은 일반 F # (또는 일반 .NET) API를 보유한 다음 다른 스타일에서 자연스럽게 사용할 수 있도록 래퍼를 제공하는 것입니다. 래퍼는 별도의 네임 스페이스 (같은에있을 수 MyLibrary.FSharpMyLibrary).

예제에서는 F # 구현을 그대로 MyLibrary.FSharp둔 다음 .NET (C # 친화적) 래퍼 (Daniel이 게시 한 코드와 유사)를 MyLibrary네임 스페이스에 일부 클래스의 정적 메서드로 추가 할 수 있습니다. 그러나 .NET 라이브러리는 아마도 함수 구성보다 더 구체적인 API를 가질 것입니다.


1
F # 구성 요소 디자인 지침은 이제 여기에서
Jan Schiefer

19

함수 (부분적으로 적용된 함수 등)을 Func또는 로 감싸기 만하면 Action나머지는 자동으로 변환됩니다. 예를 들면 :

type A(arg) =
  member x.Invoke(f: Func<_,_>) = f.Invoke(arg)

let a = A(1)
a.Invoke(fun i -> i + 1)

따라서 해당되는 경우 Func/ 를 사용하는 것이 좋습니다 Action. 이것이 당신의 우려를 제거합니까? 제안 된 솔루션이 지나치게 복잡하다고 생각합니다. 전체 라이브러리를 F #으로 작성하고 F # C # 에서 고통없이 사용할 수 있습니다 (항상 수행합니다).

또한 F #은 상호 운용성 측면에서 C #보다 유연하므로 일반적으로 이것이 우려되는 경우 기존 .NET 스타일을 따르는 것이 가장 좋습니다.

편집하다

별도의 네임 스페이스에서 두 개의 공용 인터페이스를 만드는 데 필요한 작업은 상호 보완 적이거나 F # 기능을 C #에서 사용할 수없는 경우에만 보장됩니다 (예 : F # 관련 메타 데이터에 의존하는 인라인 함수).

차례대로 포인트 취하기 :

  1. 모듈 + let바인딩 및 생성자없는 형식 + 정적 멤버는 C #에서 정확히 동일하게 표시되므로 가능하면 모듈을 사용하세요. CompiledNameAttribute멤버에게 C # 친화적 인 이름을 지정 하는 데 사용할 수 있습니다 .

  2. 내가 틀렸을 수도 있지만 System.Tuple, 프레임 워크에 추가 되기 전에 컴포넌트 가이드 라인이 작성되었다고 생각 합니다. (이전 버전에서는 F #이 자체 튜플 형식을 정의했습니다.) 이후 Tuple로 사소한 형식에 대해 공용 인터페이스에서 사용하는 것이 더 적합 해졌습니다 .

  3. F #은 잘 Task작동하지만 C #은 .NET과 잘 작동하지 않기 때문에 C # 방식으로 작업을 수행했다고 생각합니다 Async. 내부적으로 비동기를 사용한 다음 Async.StartAsTask공용 메서드에서 반환하기 전에 호출 할 수 있습니다 .

  4. nullC #에서 사용할 API를 개발할 때 가장 큰 단점은 포용 일 수 있습니다. 과거에는 내부 F # 코드에서 null을 고려하지 않기 위해 모든 종류의 트릭을 시도했지만 결국에는 공용 생성자로 형식을 표시 [<AllowNullLiteral>]하고 null에 대한 인수를 확인 하는 것이 가장 좋습니다 . 이 점에서 C #보다 나쁘지 않습니다.

  5. 차별 공용체는 일반적으로 클래스 계층 구조로 컴파일되지만 항상 C #에서 비교적 친숙한 표현을 사용합니다. 나는 그것들을 표시 [<AllowNullLiteral>]하고 사용 한다고 말할 것입니다.

  6. Curried 함수는 사용해서는 안되는 함수 값을 생성합니다 .

  7. 공개 인터페이스에서 잡히고 내부적으로 무시하는 것보다 null을 수용하는 것이 더 낫다는 것을 알았습니다. YMMV.

  8. 내부적으로 list/ map/ 사용하는 것이 좋습니다 set. 모두 공용 인터페이스를 통해 IEnumerable<_>. 또한 seq, dict, 그리고 Seq.readonly자주 유용하다.

  9. # 6을 참조하십시오.

어떤 전략을 선택 하느냐는 라이브러리의 유형과 크기에 따라 다르지만 제 경험상 F #과 C # 사이의 최적 지점을 찾는 데는 장기적으로 별도의 API를 만드는 것보다 적은 작업이 필요했습니다.


예, 일을 할 수있는 몇 가지 방법이 있음을 알 수 있습니다. 전적으로 확신하지는 않습니다. Re 모듈. 그렇다면 module Foo.Bar.ZooC #에서 이것은 class Foonamespace에 있을 것 입니다 Foo.Bar. 이 같은 중첩 된 모듈이 경우 module Foo=... module Bar=... module Zoo==,이 클래스가 생성됩니다 Foo내부 클래스로 BarZoo. Re IEnumerable, 이것은지도와 세트로 작업해야 할 때 허용되지 않을 수 있습니다. Re null값과 AllowNullLiteral-내가 F #을 좋아하는 한 가지 이유 null는 C #에 지 쳤기 때문에 실제로 모든 것을 다시 오염시키고 싶지 않기 때문입니다!
Philip P.

일반적으로 interop을 위해 중첩 된 모듈보다 네임 스페이스를 선호합니다. Re : null, 동의합니다. 확실히 고려해야 할 회귀입니다.하지만 API가 C #에서 사용되는 경우에는 처리해야합니다.
다니엘

2

과잉 일 수도 있지만 Mono.Cecil을 사용하여 애플리케이션을 작성하는 것을 고려할 수 있습니다. 있지만 IL 수준에서 변환을 자동화하는 (메일 링리스트에 대한 멋진 지원이 있음)을 있습니다. 예를 들어 F # 스타일의 공용 API를 사용하여 F #에서 어셈블리를 구현하면 도구가 그 위에 C # 친화적 인 래퍼를 생성합니다.

예를 들어 F # 에서는 C #에서 like 를 사용하는 대신 분명히 option<'T>( None, 특히)를 사용 null합니다. 이 시나리오를위한 래퍼 생성기를 작성하는 것은 매우 쉬울 것입니다. 래퍼 메서드는 원래 메서드를 호출합니다. 반환 값이 Some x이면 return x, 그렇지 않으면 return null입니다.

T가 값 유형 인 경우, 즉 널이 불가능한 경우를 처리해야합니다 . 래퍼 메서드의 반환 값을로 래핑 Nullable<T>해야하므로 약간 고통 스럽습니다.

다시 말하지만, 이러한 라이브러리 (F # 및 C # 둘 다에서 원활하게 사용 가능)에서 정기적으로 작업하는 경우를 제외하고는 시나리오에서 이러한 도구를 작성하는 것이 효과가있을 것이라고 확신합니다. 어쨌든, 나는 언젠가 탐구 할 수도있는 흥미로운 실험이 될 것이라고 생각합니다.


흥미롭게 들립니다. 사용 Mono.Cecil한다는 것은 기본적으로 F # 어셈블리를로드하고 매핑이 필요한 항목을 찾고 C # 래퍼로 다른 어셈블리를 내보내는 프로그램을 작성하는 것을 의미합니까? 내가 맞았나?
Philip P.

@Komrade P .: 당신도 그렇게 할 수 있지만 그것은 훨씬 더 복잡합니다. 새로운 어셈블리의 멤버를 가리 키도록 모든 참조를 조정해야하기 때문입니다. 새 멤버 (래퍼)를 기존 F #으로 작성된 어셈블리에 추가하기 만하면 훨씬 간단합니다.
ShdNx

2

F # 구성 요소 디자인 지침 초안 (2010 년 8 월)

개요이 문서에서는 F # 구성 요소 디자인 및 코딩과 관련된 몇 가지 문제를 살펴 봅니다. 특히 다음 사항을 다룹니다.

  • 모든 .NET 언어에서 사용할 "바닐라".NET 라이브러리를 설계하기위한 지침.
  • F # -F # 라이브러리 및 F # 구현 코드에 대한 지침.
  • F # 구현 코드의 코딩 규칙에 대한 제안
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.