Phương pháp tốt nhất để thiết kế thư viện F # để sử dụng từ cả F # và C #


113

Tôi đang cố gắng thiết kế một thư viện trong F #. Thư viện phải thân thiện để sử dụng từ cả F # và C # .

Và đây là nơi tôi bị mắc kẹt một chút. Tôi có thể làm cho nó thân thiện với F #, hoặc tôi có thể làm cho nó thân thiện với C #, nhưng vấn đề là làm sao để nó thân thiện với cả hai.

Đây là một ví dụ. Hãy tưởng tượng tôi có hàm sau trong F #:

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

Điều này hoàn toàn có thể sử dụng được từ F #:

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

Trong C #, composehàm có chữ ký sau:

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

Nhưng tất nhiên, tôi không muốn có FSharpFuncchữ ký, những gì tôi muốn là FuncActionthay vào đó, như thế này:

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

Để đạt được điều này, tôi có thể tạo compose2hàm như sau:

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

Bây giờ, điều này hoàn toàn có thể sử dụng được trong C #:

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

Nhưng bây giờ chúng ta gặp sự cố khi sử dụng compose2từ F #! Bây giờ tôi phải gói tất cả F # tiêu chuẩn funsvào FuncAction, như thế này:

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"

Câu hỏi: Làm thế nào chúng ta có thể đạt được trải nghiệm hạng nhất cho thư viện được viết bằng F # cho cả người dùng F # và C #?

Cho đến nay, tôi không thể nghĩ ra điều gì tốt hơn hai cách tiếp cận sau:

  1. Hai tập hợp riêng biệt: một tập hợp nhắm mục tiêu đến người dùng F # và tập hợp thứ hai nhắm mục tiêu đến người dùng C #.
  2. Một tập hợp nhưng không gian tên khác nhau: một cho người dùng F # và một cho người dùng C #.

Đối với cách tiếp cận đầu tiên, tôi sẽ làm như sau:

  1. Tạo một dự án F #, gọi nó là FooBarFs và biên dịch nó thành FooBarFs.dll.

    • Nhắm mục tiêu thư viện hoàn toàn đến người dùng F #.
    • Ẩn mọi thứ không cần thiết khỏi tệp .fsi.
  2. Tạo một dự án F # khác, gọi if FooBarCs và biên dịch nó thành FooFar.dll

    • Sử dụng lại dự án F # đầu tiên ở cấp nguồn.
    • Tạo tệp .fsi ẩn mọi thứ khỏi dự án đó.
    • Tạo tệp .fsi hiển thị thư viện theo cách C #, sử dụng các thành ngữ C # cho tên, không gian tên, v.v.
    • Tạo các trình bao bọc ủy quyền cho thư viện lõi, thực hiện chuyển đổi khi cần thiết.

Tôi nghĩ rằng cách tiếp cận thứ hai với không gian tên có thể gây nhầm lẫn cho người dùng, nhưng sau đó bạn có một tập hợp.

Câu hỏi: Không có cái nào trong số này là lý tưởng, có lẽ tôi đang thiếu một số loại cờ / chuyển đổi / thuộc tính của trình biên dịch hoặc một số loại thủ thuật và có một cách tốt hơn để làm điều này?

Câu hỏi: đã có ai khác cố gắng đạt được điều gì đó tương tự chưa và nếu có thì bạn đã làm như thế nào?

CHỈNH SỬA: để làm rõ, câu hỏi không chỉ về các hàm và đại biểu mà còn là trải nghiệm tổng thể của người dùng C # với thư viện F #. Điều này bao gồm không gian tên, quy ước đặt tên, thành ngữ và những thứ tương tự như vậy có nguồn gốc từ C #. Về cơ bản, người dùng C # sẽ không thể phát hiện ra rằng thư viện được tạo ra trong F #. Và ngược lại, người dùng F # sẽ cảm thấy thích giao dịch với thư viện C #.


CHỈNH SỬA 2:

Tôi có thể thấy từ các câu trả lời và nhận xét cho đến nay rằng câu hỏi của tôi thiếu độ sâu cần thiết, có lẽ chủ yếu là do chỉ sử dụng một ví dụ trong đó các vấn đề về khả năng tương tác giữa F # và C # phát sinh, vấn đề về giá trị hàm. Tôi nghĩ đây là ví dụ rõ ràng nhất và vì vậy điều này đã khiến tôi sử dụng nó để đặt câu hỏi, nhưng cùng một dấu hiệu đã tạo ấn tượng rằng đây là vấn đề duy nhất tôi quan tâm.

Hãy để tôi cung cấp các ví dụ cụ thể hơn. Tôi đã đọc qua tài liệu Hướng dẫn thiết kế thành phần F # tuyệt vời nhất (rất cám ơn @gradbot về điều này!). Các hướng dẫn trong tài liệu, nếu được sử dụng, sẽ giải quyết một số vấn đề nhưng không phải tất cả.

Tài liệu được chia thành hai phần chính: 1) hướng dẫn nhắm mục tiêu người dùng F #; và 2) hướng dẫn nhắm mục tiêu người dùng C #. Không nơi nào nó thậm chí cố gắng giả vờ rằng có thể có một cách tiếp cận thống nhất, điều này lặp lại chính xác câu hỏi của tôi: chúng ta có thể nhắm mục tiêu F #, chúng ta có thể nhắm mục tiêu C #, nhưng giải pháp thực tế để nhắm mục tiêu cả hai là gì?

Để nhắc nhở, mục tiêu là có một thư viện được tạo ra bằng F # và có thể được sử dụng thành ngữ từ cả hai ngôn ngữ F # và C #.

Từ khóa ở đây là thành ngữ . Vấn đề không phải là khả năng tương tác chung mà chỉ có thể sử dụng các thư viện bằng các ngôn ngữ khác nhau.

Bây giờ đến các ví dụ, mà tôi lấy ngay từ Hướng dẫn thiết kế thành phần F # .

  1. Mô-đun + chức năng (F #) so với Không gian tên + Loại + chức năng

    • F #: Sử dụng không gian tên hoặc mô-đun để chứa các loại và mô-đun của bạn. Cách sử dụng thành ngữ là đặt các chức năng trong các mô-đun, ví dụ:

      // library
      module Foo
      let bar() = ...
      let zoo() = ...
      
      
      // Use from F#
      open Foo
      bar()
      zoo()
    • C #: Sử dụng không gian tên, kiểu và thành viên làm cấu trúc tổ chức chính cho các thành phần của bạn (trái ngược với mô-đun), cho các API .NET vani.

      Điều này không tương thích với hướng dẫn F # và ví dụ sẽ cần được viết lại để phù hợp với người dùng C #:

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

      Mặc dù vậy, bằng cách làm như vậy, chúng ta phá vỡ cách sử dụng thành ngữ từ F # vì chúng ta không thể sử dụng nữa barzookhông có tiền tố Foo.

  2. Sử dụng bộ giá trị

    • F #: Sử dụng bộ giá trị khi thích hợp cho các giá trị trả về.

    • C #: Tránh sử dụng các bộ giá trị làm giá trị trả về trong các API .NET vani.

  3. Không đồng bộ

    • F #: Sử dụng Async để lập trình không đồng bộ ở các ranh giới API F #.

    • C #: Hiển thị các hoạt động không đồng bộ bằng cách sử dụng mô hình lập trình không đồng bộ .NET (BeginFoo, EndFoo) hoặc dưới dạng các phương thức trả về các tác vụ .NET (Task), chứ không phải là các đối tượng F # Async.

  4. Sử dụng Option

    • F #: Xem xét sử dụng các giá trị tùy chọn cho các kiểu trả về thay vì nâng cao các ngoại lệ (đối với mã F # -facing).

    • Cân nhắc sử dụng mẫu TryGetValue thay vì trả về giá trị tùy chọn F # (tùy chọn) trong các API .NET vani và thích nạp chồng phương thức hơn việc lấy các giá trị tùy chọn F # làm đối số.

  5. Công đoàn phân biệt đối xử

    • F #: Sử dụng các liên hiệp phân biệt đối xử như một sự thay thế cho phân cấp lớp để tạo dữ liệu có cấu trúc cây

    • C #: không có hướng dẫn cụ thể cho việc này, nhưng khái niệm về công đoàn bị phân biệt đối xử là xa lạ với C #

  6. Chức năng làm xoăn

    • F #: hàm cà ri là thành ngữ cho F #

    • C #: Không sử dụng các tham số trong các API .NET vani.

  7. Kiểm tra giá trị null

    • F #: đây không phải là thành ngữ cho F #

    • C #: Xem xét kiểm tra các giá trị null trên các ranh giới API .NET vani.

  8. Sử dụng F # loại list, map, set, vv

    • F #: sử dụng chúng trong F # là thành ngữ

    • C #: Cân nhắc sử dụng các loại giao diện thu thập .NET IEnumerable và IDictionary cho các tham số và giá trị trả về trong các API .NET vani. ( Tức là không sử dụng F # list, map,set )

  9. Các kiểu hàm (cái rõ ràng)

    • F #: việc sử dụng các hàm F # làm các giá trị là thành ngữ cho F #, rõ ràng

    • C #: Sử dụng kiểu ủy quyền .NET thay vì kiểu hàm F # trong các API .NET vani.

Tôi nghĩ những điều này đủ để chứng minh bản chất của câu hỏi của tôi.

Ngẫu nhiên, các hướng dẫn cũng có một phần câu trả lời:

... một chiến lược triển khai phổ biến khi phát triển các phương thức bậc cao hơn cho thư viện vanilla .NET là tác giả tất cả việc triển khai bằng cách sử dụng các kiểu hàm F #, sau đó tạo API công khai bằng cách sử dụng các đại biểu như một mặt tiền mỏng ở phía trên thực hiện F #.

Để tóm tắt.

Có một câu trả lời chắc chắn: không có thủ thuật biên dịch nào mà tôi đã bỏ qua .

Theo tài liệu hướng dẫn, có vẻ như việc tạo tác giả cho F # trước và sau đó tạo một trình bao bọc mặt tiền cho .NET là một chiến lược hợp lý.

Câu hỏi sau đó vẫn liên quan đến việc triển khai thực tế điều này:

  • Lắp ráp riêng biệt? hoặc là

  • Không gian tên khác nhau?

Nếu giải thích của tôi là đúng, Tomas gợi ý rằng việc sử dụng các không gian tên riêng biệt là đủ và phải là một giải pháp chấp nhận được.

Tôi nghĩ rằng tôi sẽ đồng ý với điều đó vì việc lựa chọn không gian tên sao cho nó không gây ngạc nhiên hoặc nhầm lẫn cho người dùng .NET / C #, điều đó có nghĩa là không gian tên cho họ có thể trông giống như nó là không gian tên chính cho họ. Người dùng F # sẽ phải chịu gánh nặng của việc chọn không gian tên cụ thể F #. Ví dụ:

  • FSharp.Foo.Bar -> không gian tên cho F # đối diện với thư viện

  • Foo.Bar -> không gian tên cho trình bao bọc .NET, thành ngữ cho C #



Ngoài việc chuyển xung quanh các hàm hạng nhất, bạn còn lo lắng gì nữa? F # đã tiến một bước dài trong việc cung cấp trải nghiệm liền mạch từ các ngôn ngữ khác.
Daniel

Re: bản chỉnh sửa của bạn, tôi vẫn chưa rõ tại sao bạn nghĩ rằng một thư viện được tác giả trong F # sẽ có vẻ không chuẩn từ C #. Đối với hầu hết các phần, F # cung cấp một tập hợp siêu chức năng của C #. Làm cho thư viện cảm thấy tự nhiên chủ yếu là một vấn đề của quy ước, điều này đúng bất kể bạn đang sử dụng ngôn ngữ nào. Tất cả những điều cần nói, F # cung cấp tất cả các công cụ bạn cần để thực hiện việc này.
Daniel

Thành thật mà nói, mặc dù bạn bao gồm một mẫu mã, nhưng bạn đang hỏi một câu hỏi khá mở. Bạn hỏi về "trải nghiệm tổng thể của người dùng C # với thư viện F #" nhưng ví dụ duy nhất bạn đưa ra là một thứ thực sự không được hỗ trợ tốt trong bất kỳ ngôn ngữ mệnh lệnh nào . Đối xử với các chức năng như những công dân hạng nhất là một nguyên lý khá trung tâm của lập trình chức năng - thứ ánh xạ kém với hầu hết các ngôn ngữ mệnh lệnh.
Onorio Catenacci

2
@KomradeP .: liên quan đến EDIT 2 của bạn, FSharpx cung cấp một số phương tiện để làm cho khả năng tương tác trở nên vô nghĩa hơn. Bạn có thể tìm thấy một số gợi ý trong bài đăng blog xuất sắc này .
pad

Câu trả lời:


40

Daniel đã giải thích cách xác định phiên bản thân thiện với C # của hàm F # mà bạn đã viết, vì vậy tôi sẽ thêm một số nhận xét cấp cao hơn. Trước hết, bạn nên đọc Hướng dẫn thiết kế thành phần F # (đã được tham chiếu bởi gradbot). Đây là tài liệu giải thích cách thiết kế thư viện F # và .NET bằng F # và nó sẽ trả lời nhiều câu hỏi của bạn.

Khi sử dụng F #, về cơ bản có hai loại thư viện bạn có thể viết:

  • Thư viện F # được thiết kế để chỉ sử dụng từ F #, do đó, giao diện công cộng của nó được viết theo kiểu chức năng (sử dụng các kiểu hàm F #, bộ giá trị, kết hợp phân biệt đối xử, v.v.)

  • Thư viện .NET được thiết kế để sử dụng từ bất kỳ ngôn ngữ .NET nào (bao gồm cả C # và F #) và nó thường tuân theo kiểu hướng đối tượng .NET. Điều này có nghĩa là bạn sẽ hiển thị hầu hết các chức năng dưới dạng các lớp với phương thức (và đôi khi là các phương thức mở rộng hoặc phương thức tĩnh, nhưng phần lớn mã phải được viết trong thiết kế OO).

Trong câu hỏi của bạn, bạn đang hỏi làm thế nào để hiển thị thành phần hàm dưới dạng thư viện .NET, nhưng tôi nghĩ rằng các hàm như của bạn composelà khái niệm cấp quá thấp theo quan điểm thư viện .NET. Bạn có thể hiển thị chúng dưới dạng các phương thức làm việc với FuncAction, nhưng đó có thể không phải là cách bạn thiết kế một thư viện .NET bình thường ngay từ đầu (có lẽ bạn sẽ sử dụng mẫu Builder để thay thế hoặc tương tự).

Trong một số trường hợp (tức là khi thiết kế các thư viện số không thực sự phù hợp với kiểu thư viện .NET), bạn nên thiết kế một thư viện kết hợp cả kiểu F #.NET trong một thư viện. Cách tốt nhất để làm điều này là có API F # (hoặc .NET bình thường) bình thường và sau đó cung cấp các trình bao bọc để sử dụng tự nhiên theo kiểu khác. Các trình bao bọc có thể nằm trong một không gian tên riêng biệt (như MyLibrary.FSharpMyLibrary).

Trong ví dụ của bạn, bạn có thể để triển khai F # MyLibrary.FSharpvà sau đó thêm trình bao bọc .NET (C # -friendly) (tương tự như mã mà Daniel đã đăng) trong MyLibrarykhông gian tên dưới dạng phương thức tĩnh của một số lớp. Nhưng một lần nữa, thư viện .NET có thể sẽ có API cụ thể hơn là thành phần hàm.


1
Hướng dẫn thiết kế thành phần F # hiện được lưu trữ tại đây
Jan Schiefer

19

Bạn chỉ phải bọc các giá trị hàm (các hàm được áp dụng một phần, v.v.) với Funchoặc Action, phần còn lại được chuyển đổi tự động. Ví dụ:

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

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

Vì vậy, nó là hợp lý để sử dụng Func/ Actionnếu áp dụng. Điều này có loại bỏ mối quan tâm của bạn? Tôi nghĩ rằng các giải pháp được đề xuất của bạn là quá phức tạp. Bạn có thể viết toàn bộ thư viện của mình bằng F # và sử dụng nó mà không bị đau từ F # C # (tôi luôn làm điều đó).

Ngoài ra, F # linh hoạt hơn C # về khả năng tương tác, do đó, tốt nhất là bạn nên làm theo kiểu .NET truyền thống khi điều này là đáng lo ngại.

BIÊN TẬP

Tôi nghĩ rằng công việc cần thiết để tạo hai giao diện công khai trong các không gian tên riêng biệt chỉ được đảm bảo khi chúng bổ sung hoặc chức năng F # không thể sử dụng được từ C # (chẳng hạn như các hàm nội tuyến, phụ thuộc vào siêu dữ liệu cụ thể F #).

Lần lượt lấy điểm của bạn:

  1. Mô-đun + let ràng buộc và kiểu không hàm tạo + thành viên tĩnh xuất hiện hoàn toàn giống nhau trong C #, vì vậy hãy sử dụng mô-đun nếu bạn có thể. Bạn có thể sử dụng CompiledNameAttributeđể đặt tên thân thiện cho các thành viên C #.

  2. Tôi có thể sai, nhưng tôi đoán là Nguyên tắc thành phần đã được viết trước khi System.Tupleđược thêm vào khuôn khổ. (Trong các phiên bản trước đó F # đã định nghĩa đó là kiểu tuple riêng.) Kể từ đó nó trở nên dễ chấp nhận hơn khi sử dụng Tupletrong giao diện công cộng cho các kiểu tầm thường.

  3. Đây là lúc tôi nghĩ bạn có thể làm mọi thứ theo cách của C # vì F # chơi tốt Tasknhưng C # không chơi tốt Async. Bạn có thể sử dụng async nội bộ sau đó gọiAsync.StartAsTask trước khi quay lại từ một phương thức công khai.

  4. Ôm lấy null có thể là nhược điểm lớn nhất khi phát triển API để sử dụng từ C #. Trước đây, tôi đã thử tất cả các thủ thuật để tránh xem xét null trong mã F # nội bộ, nhưng cuối cùng, tốt nhất là đánh dấu các loại bằng các hàm tạo công khai với [<AllowNullLiteral>]và kiểm tra các args cho null. Nó không tệ hơn C # về mặt này.

  5. Các công đoàn bị phân biệt đối xử thường được biên dịch theo phân cấp lớp nhưng luôn có một đại diện tương đối thân thiện trong C #. Tôi sẽ nói, đánh dấu chúng bằng[<AllowNullLiteral>] và sử dụng chúng.

  6. Các hàm được kết hợp tạo ra các giá trị hàm , những này không nên được sử dụng.

  7. Tôi thấy tốt hơn là nên nắm lấy null hơn là phụ thuộc vào việc nó bị bắt ở giao diện công khai và bỏ qua nó trong nội bộ. YMMV.

  8. Nó rất có ý nghĩa khi sử dụng list/ map/ settrong nội bộ. Tất cả chúng có thể được hiển thị thông qua giao diện công khai như IEnumerable<_>. Ngoài ra, seq, dict, vàSeq.readonly thường xuyên rất hữu ích.

  9. Xem # 6.

Bạn thực hiện chiến lược nào phụ thuộc vào loại và kích thước thư viện của bạn, nhưng theo kinh nghiệm của tôi, việc tìm ra điểm hợp lý giữa F # và C # cần ít công việc hơn — về lâu dài — hơn là tạo các API riêng biệt.


vâng, tôi có thể thấy có một số cách để làm cho mọi thứ hoạt động, tôi không hoàn toàn bị thuyết phục. Re các mô-đun. Nếu bạn có module Foo.Bar.Zoothì trong C #, nó sẽ nằm class Footrong không gian tên Foo.Bar. Tuy nhiên nếu bạn có các mô-đun lồng nhau như thế module Foo=... module Bar=... module Zoo==, điều này sẽ tạo ra lớp Foovới các lớp bên trong BarZoo. Re IEnumerable, điều này có thể không được chấp nhận khi chúng ta thực sự cần làm việc với bản đồ và bộ. Đánh nullgiá lại và AllowNullLiteral- một lý do tôi thích F # là vì tôi quá mệt mỏi với nullC #, thực sự không muốn làm ô nhiễm mọi thứ với nó một lần nữa!
Philip P.

Nói chung ưu tiên không gian tên hơn các mô-đun lồng nhau cho tương tác. Re: null, tôi đồng ý với bạn, đó chắc chắn là một hồi quy để phải xem xét nó, nhưng bạn phải giải quyết một số thứ nếu API của bạn sẽ được sử dụng từ C #.
Daniel

2

Mặc dù nó có thể là một việc làm quá mức cần thiết, nhưng bạn có thể cân nhắc việc viết một ứng dụng bằng Mono.Cecil (nó có hỗ trợ tuyệt vời trên danh sách gửi thư) sẽ tự động chuyển đổi ở cấp IL. Ví dụ: bạn triển khai hợp ngữ của mình trong F #, sử dụng API công khai kiểu F #, sau đó công cụ sẽ tạo một trình bao bọc thân thiện với C # trên nó.

Ví dụ, trong F #, bạn rõ ràng sẽ sử dụng option<'T>( None, cụ thể) thay vì sử dụng nullnhư trong C #. Việc viết trình tạo trình bao bọc cho trường hợp này khá dễ dàng: phương thức trình bao bọc sẽ gọi phương thức ban đầu: nếu nó là giá trị Some xtrả về thì hãy trả về x, nếu không thì trả về null.

Bạn sẽ cần phải xử lý trường hợp khi Tlà một kiểu giá trị, tức là không thể null; bạn sẽ phải bọc giá trị trả về của phương thức wrapper vào Nullable<T>, điều này làm cho nó hơi đau.

Một lần nữa, tôi khá chắc chắn rằng sẽ có lợi khi viết một công cụ như vậy trong kịch bản của bạn, có thể ngoại trừ trường hợp bạn sẽ làm việc trên thư viện như vậy (có thể sử dụng liền mạch từ F # và C #) thường xuyên. Trong mọi trường hợp, tôi nghĩ đó sẽ là một thử nghiệm thú vị, một thử nghiệm mà đôi khi tôi có thể khám phá.


Nghe có vẻ thú vị. Sẽ sử dụng Mono.Ceciltrung bình cơ bản viết một chương trình để tải F # lắp ráp, tìm các mục yêu cầu lập bản đồ, và họ phát ra lắp ráp khác với C # wrappers? Tôi đã hiểu đúng?
Philip P.

@Komrade P.: Bạn cũng có thể làm điều đó, nhưng điều đó phức tạp hơn nhiều, vì sau đó bạn phải điều chỉnh tất cả các tham chiếu để trỏ đến các thành viên của hội mới. Sẽ đơn giản hơn nhiều nếu bạn chỉ cần thêm các thành viên mới (các trình bao bọc) vào hợp ngữ viết bằng F # hiện có.
ShdNx

2

Dự thảo Hướng dẫn Thiết kế Thành phần F # (tháng 8 năm 2010)

Tổng quan Tài liệu này xem xét một số vấn đề liên quan đến thiết kế và mã hóa thành phần F #. Đặc biệt, nó bao gồm:

  • Hướng dẫn thiết kế thư viện .NET “vani” để sử dụng từ bất kỳ ngôn ngữ .NET nào.
  • Hướng dẫn về thư viện F # -to-F # và mã triển khai F #.
  • Gợi ý về quy ước mã hóa cho mã triển khai F #
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.