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
서명을 원하지 않습니다. 내가 원하는 것은 다음 Func
과 Action
같습니다.
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);
}
하지만 이제 compose2
F #에서 사용하는 데 문제가 있습니다 ! 이제 funs
다음 Func
과 Action
같이 모든 표준 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 #으로 작성된 라이브러리의 최고 수준의 경험을 얻으려면 어떻게해야합니까?
지금까지이 두 가지 접근 방식보다 더 나은 방법을 찾을 수 없었습니다.
- 두 개의 개별 어셈블리 : 하나는 F # 사용자를 대상으로하고 다른 하나는 C # 사용자를 대상으로합니다.
- 하나의 어셈블리이지만 다른 네임 스페이스 : 하나는 F # 사용자 용이고 다른 하나는 C # 사용자 용입니다.
첫 번째 접근 방식의 경우 다음과 같이합니다.
F # 프로젝트를 만들고 FooBarFs라고 부르고 FooBarFs.dll로 컴파일합니다.
- 순수하게 F # 사용자에게만 라이브러리를 대상으로 지정합니다.
- .fsi 파일에서 불필요한 모든 것을 숨 깁니다.
다른 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 에서 직접 가져온 예제로 이동합니다 .
모듈 + 함수 (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 번호의 관용적 인 사용을 중단 우리가 더 이상 사용을 할 수 있기 때문에
bar
과zoo
를 앞에없이Foo
.
튜플 사용
F # : 반환 값에 적절한 경우 튜플을 사용합니다.
C # : vanilla .NET API에서 튜플을 반환 값으로 사용하지 마십시오.
비동기
F # : F # API 경계에서 비동기 프로그래밍에 Async를 사용하세요.
C # : .NET 비동기 프로그래밍 모델 (BeginFoo, EndFoo)을 사용하거나 F # Async 개체가 아닌 .NET 작업 (Task)을 반환하는 메서드를 사용하여 비동기 작업을 노출합니다.
사용
Option
F # : 예외를 발생시키는 대신 반환 형식에 옵션 값을 사용하는 것이 좋습니다 (F # 관련 코드의 경우).
바닐라 .NET API에서 F # 옵션 값 (옵션)을 반환하는 대신 TryGetValue 패턴을 사용하고 F # 옵션 값을 인수로 사용하는 것보다 메서드 오버로드를 선호합니다.
차별적 노조
F # : 트리 구조 데이터를 만드는 데 클래스 계층 구조 대신 구분 된 공용체를 사용하세요.
C # : 이에 대한 구체적인 지침은 없지만 차별 된 공용체의 개념은 C #과 는 다릅니다.
카레 기능
F # : 커리 함수는 F #의 관용적입니다.
C # : 바닐라 .NET API에서 매개 변수 커링을 사용하지 마십시오.
null 값 확인
F # : 이것은 F #의 관용적이지 않습니다.
C # : 바닐라 .NET API 경계에서 null 값을 확인하는 것이 좋습니다.
F 번호 유형의 사용
list
,map
,set
, 등F # : F # 에서 사용하는 것은 관용적입니다.
C # : 바닐라 .NET API의 매개 변수 및 반환 값에 .NET 컬렉션 인터페이스 유형 IEnumerable 및 IDictionary를 사용하는 것이 좋습니다. ( 즉, F 번호를 사용하지 않는
list
,map
,set
)
함수 유형 (명백한 유형)
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 #의 관용적