Hem F # hem de C # ile kullanılmak üzere F # kitaplıkları tasarlamak için en iyi yaklaşım


113

F # ile bir kütüphane tasarlamaya çalışıyorum. Kitaplık, hem F # hem de C # için uygun olmalıdır .

Ve burası biraz sıkıştığım yer. Bunu F # dostu yapabilirim veya C # dostu yapabilirim, ancak sorun her ikisi için de nasıl kolay hale getirileceğidir.

İşte bir örnek. F # 'da aşağıdaki işleve sahip olduğumu hayal edin:

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

Bu, F # 'dan mükemmel şekilde kullanılabilir:

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

C # 'da composeişlev aşağıdaki imzaya sahiptir:

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

Ama elbette FSharpFuncimzayı istemiyorum, istediğim şey şu Funcve Actionbunun yerine:

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

Bunu başarmak için şöyle bir compose2işlev oluşturabilirim :

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

Şimdi, bu C # 'da mükemmel şekilde kullanılabilir:

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

Ama şimdi compose2F # kullanarak bir sorunumuz var ! Şimdi tüm standart F # sarmak zorunda funsiçine Funcve Actionbunun gibi,:

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"

Soru: Hem F # hem de C # kullanıcıları için F # ile yazılmış kitaplık için birinci sınıf deneyimi nasıl elde edebiliriz?

Şimdiye kadar, bu iki yaklaşımdan daha iyisini bulamadım:

  1. İki ayrı derleme: biri F # kullanıcılarını ve ikincisi C # kullanıcılarını hedefliyor.
  2. Bir derleme ancak farklı ad alanları: biri F # kullanıcıları için ve ikincisi C # kullanıcıları için.

İlk yaklaşım için şöyle bir şey yapardım:

  1. Bir F # projesi oluşturun, buna FooBarFs adını verin ve FooBarFs.dll'de derleyin.

    • Kitaplığı tamamen F # kullanıcılarına hedefleyin.
    • .Fsi dosyalarından gereksiz her şeyi gizleyin.
  2. Başka bir F # projesi oluşturun, FooBarCs'yi arayın ve FooFar.dll'de derleyin

    • Kaynak düzeyinde ilk F # projesini yeniden kullanın.
    • Bu projedeki her şeyi gizleyen .fsi dosyası oluşturun.
    • Ad, ad alanları vb. İçin C # deyimlerini kullanarak kitaplığı C # şeklinde ortaya çıkaran .fsi dosyası oluşturun.
    • Çekirdek kitaplığa yetki veren sarmalayıcılar oluşturun ve gerektiğinde dönüşümü gerçekleştirin.

Ad alanlarıyla ilgili ikinci yaklaşımın kullanıcılar için kafa karıştırıcı olabileceğini düşünüyorum, ancak o zaman bir derlemeniz var.

Soru: Bunların hiçbiri ideal değil, belki bir tür derleyici bayrağı / anahtarı / özniteliği veya bir tür hile özlüyorum ve bunu yapmanın daha iyi bir yolu var mı?

Soru: başka biri benzer bir şeyi başarmaya çalıştı mı ve öyleyse bunu nasıl yaptınız?

DÜZENLEME: Açıklığa kavuşturmak gerekirse, soru yalnızca işlevler ve temsilcilerle ilgili değil, aynı zamanda bir F # kitaplığına sahip bir C # kullanıcısının genel deneyimiyle ilgili. Bu, ad alanlarını, adlandırma kurallarını, deyimleri ve C # için yerel olan benzerlerini içerir. Temel olarak, bir C # kullanıcısı kitaplığın F # ile yazıldığını algılayamamalıdır. Ve bunun tersi, bir F # kullanıcısı bir C # kitaplığıyla uğraşıyormuş gibi hissetmelidir.


DÜZENLEME 2:

Şimdiye kadarki cevaplardan ve yorumlardan, sorumun gerekli derinliğe sahip olmadığını görebiliyorum, belki de çoğunlukla F # ve C # arasındaki birlikte çalışabilirlik sorunlarının ortaya çıktığı tek bir örnek, fonksiyon değerleri sorunu nedeniyle. Bence bu en bariz örnek ve bu yüzden bu beni soruyu sormak için kullanmamı sağladı, ancak aynı şekilde ilgilendiğim tek sorunun bu olduğu izlenimini verdi.

Daha somut örnekler vereyim. En mükemmel F # Bileşen Tasarım Yönergeleri belgesini okudum (bunun için çok teşekkürler @gradbot!). Belgedeki yönergeler, kullanılıyorsa, bazı konuları ele alır, ancak hepsini değil.

Belge iki ana bölüme ayrılmıştır: 1) F # kullanıcılarını hedeflemeye yönelik yönergeler; ve 2) C # kullanıcılarını hedeflemeye yönelik yönergeler. Hiçbir yerde tek tip bir yaklaşıma sahip olmanın mümkün olduğunu iddia etmeye bile kalkışmıyor, bu da sorumu tam olarak yansıtıyor: F # hedefleyebiliriz, C # hedefleyebiliriz, ancak her ikisini de hedeflemenin pratik çözümü nedir?

Hatırlatmak gerekirse, amaç F # dilinde yazılmış ve hem F # hem de C # dillerinden deyimsel olarak kullanılabilen bir kitaplığa sahip olmaktır .

Buradaki anahtar kelime deyimseldir . Sorun, kütüphaneleri farklı dillerde kullanmanın sadece mümkün olduğu genel birlikte çalışabilirlik değildir.

Şimdi doğrudan F # Bileşen Tasarım Kılavuzlarından aldığım örneklere gelelim .

  1. Modüller + işlevler (F #) - Ad Alanları + Türler + işlevler

    • F #: Türlerinizi ve modüllerinizi içermek için ad alanları veya modüller kullanın. Deyimsel kullanım, işlevleri modüllere yerleştirmektir, örneğin:

      // library
      module Foo
      let bar() = ...
      let zoo() = ...
      
      
      // Use from F#
      open Foo
      bar()
      zoo()
    • C #: Vanilya .NET API'leri için bileşenleriniz için (modüllerin aksine) birincil organizasyon yapısı olarak ad alanlarını, türleri ve üyeleri kullanın.

      Bu, F # yönergesi ile uyumsuzdur ve örneğin C # kullanıcılarına uyması için yeniden yazılması gerekir:

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

      Yine de bunu yaparak, deyimsel kullanımı F # 'dan ayırırız çünkü artık kullanamayız barve zooönek koymadan kullanamayız Foo.

  2. Tuple kullanımı

    • F #: Dönüş değerleri için uygun olduğunda tuple kullanın.

    • C #: Vanilya .NET API'lerinde tupleları dönüş değerleri olarak kullanmaktan kaçının.

  3. zaman uyumsuz

    • F #: F # API sınırlarında zaman uyumsuz programlama için Async kullanın.

    • C #: Zaman uyumsuz işlemleri, F # Async nesneleri yerine .NET eşzamansız programlama modelini (BeginFoo, EndFoo) veya .NET görevlerini (Task) döndüren yöntemler olarak kullanarak açığa çıkarın.

  4. Kullanımı Option

    • F #: İstisnaları artırmak yerine dönüş türleri için seçenek değerlerini kullanmayı düşünün (F # -facing kodu için).

    • Vanilya .NET API'lerinde F # seçenek değerleri (seçenek) döndürmek yerine TryGetValue modelini kullanmayı düşünün ve F # seçenek değerlerini bağımsız değişken olarak almak yerine yöntem aşırı yüklemesini tercih edin.

  5. Ayrımcı sendikalar

    • F #: Ağaç yapılı veriler oluşturmak için sınıf hiyerarşilerine alternatif olarak ayırt edici birleşimleri kullanın

    • C #: Bunun için belirli bir yönerge yok, ancak ayrımcılığa uğramış sendikalar kavramı C # için yabancı

  6. Körili fonksiyonlar

    • F #: curried işlevler F # için deyimseldir

    • C #: Vanilya .NET API'lerinde parametrelerin karıştırılmasını kullanmayın.

  7. Boş değerler kontrol ediliyor

    • F #: Bu, F # için deyimsel değildir

    • C #: Vanilya .NET API sınırlarında boş değerleri kontrol etmeyi düşünün.

  8. F # tiplerinin kullanılması list, map, setvb

    • F #: Bunları F # dilinde kullanmak deyimseldir

    • C #: Vanilla .NET API'lerinde parametreler ve dönüş değerleri için IEnumerable ve IDictionary .NET koleksiyonu arabirim türlerini kullanmayı düşünün. ( Yani F # kullanmayın list, map,set )

  9. İşlev türleri (bariz olan)

    • F #: Değerler olarak F # işlevlerinin kullanılması F # için deyimseldir, tabii ki

    • C #: Vanilya .NET API'lerinde F # işlev türleri yerine .NET temsilci türlerini kullanın.

Sorumun doğasını göstermek için bunların yeterli olması gerektiğini düşünüyorum.

Bu arada, kılavuzların da kısmi bir cevabı var:

... vanilya .NET kitaplıkları için daha yüksek sıralı yöntemler geliştirirken yaygın bir uygulama stratejisi, tüm uygulamayı F # işlev türlerini kullanarak yazmak ve ardından gerçek F # uygulamasının üzerinde ince bir cephe olarak temsilcileri kullanarak genel API'yi oluşturmaktır.

Özetle.

Kesin bir cevap var: Kaçırdığım hiçbir derleyici numarası yok .

Yönergeler belgesine göre, önce F # için yazma ve ardından .NET için bir cephe sarmalayıcı oluşturmanın makul bir strateji olduğu görülmektedir.

O zaman soru, bunun pratik uygulamasıyla ilgili kalır:

  • Ayrı montajlar mı? veya

  • Farklı ad alanları?

Yorumum doğruysa, Tomas ayrı ad alanları kullanmanın yeterli ve kabul edilebilir bir çözüm olması gerektiğini öne sürüyor.

Sanırım ad alanlarının seçiminin .NET / C # kullanıcılarını şaşırtmayacak veya şaşırtmayacak şekilde olduğu göz önüne alındığında, bu da onlar için ad alanının muhtemelen onlar için birincil ad alanı gibi görünmesi gerektiği anlamına gelir. F # kullanıcıları, F # 'ye özgü ad alanını seçme yükünü üstleneceklerdir. Örneğin:

  • FSharp.Foo.Bar -> kitaplığa bakan F # için ad alanı

  • Foo.Bar -> .NET sarmalayıcı için ad alanı, C # için deyimsel



Birinci sınıf işlevlerin etrafından dolaşmak dışında, endişeleriniz nelerdir? F #, diğer dillerden sorunsuz bir deneyim sağlama yolunda zaten uzun bir yol kat ediyor.
Daniel

Re: Düzenlemeniz, F # ile yazılmış bir kitaplığın C #'dan neden standart dışı görüneceğini düşündüğünüz konusunda hala net değilim. Çoğunlukla, F #, C # işlevinin bir üst kümesi sağlar. Bir kütüphaneyi doğal hissettirmek çoğunlukla bir gelenek meselesidir ve hangi dili kullanıyor olursanız olun bu doğrudur. Söylemek gerekirse, F # bunu yapmak için ihtiyacınız olan tüm araçları sağlar.
Daniel

Dürüst olmak gerekirse, bir kod örneği ekleseniz bile, oldukça açık uçlu bir soru soruyorsunuz. "Bir F # kitaplığına sahip bir C # kullanıcısının genel deneyimi" hakkında soru soruyorsunuz, ancak verdiğiniz tek örnek, herhangi bir zorunlu dilde gerçekten iyi desteklenmeyen bir şey . İşlevleri birinci sınıf vatandaşlar olarak ele almak, çoğu zorunlu dil ile zayıf bir şekilde eşleşen işlevsel programlamanın oldukça merkezi bir ilkesidir.
Onorio Catenacci

2
@KomradeP .: EDIT 2'nizle ilgili olarak FSharpx , birlikte çalışabilirliği daha görünür hale getirmek için bazı yollar sağlar. Bu harika blog yazısında bazı ipuçları bulabilirsiniz .
pad

Yanıtlar:


40

Daniel, yazdığınız F # işlevinin C # dostu bir sürümünü nasıl tanımlayacağınızı zaten açıkladı, bu yüzden bazı daha yüksek düzeyli yorumlar ekleyeceğim. Her şeyden önce, F # Bileşen Tasarım Yönergelerini okumalısınız (zaten gradbot tarafından başvurulan). Bu, F # kullanarak F # ve .NET kitaplıklarının nasıl tasarlanacağını açıklayan bir belgedir ve birçok sorunuza yanıt vermelidir.

F # kullanırken, yazabileceğiniz temelde iki tür kitaplık vardır:

  • F # kitaplığı yalnızca F # 'dan kullanılmak üzere tasarlanmıştır , bu nedenle genel arabirimi işlevsel bir tarzda yazılır (F # işlev türleri, tuplelar, ayırt edici birleşimler vb. Kullanılarak)

  • .NET kitaplığı , herhangi bir .NET dilinden (C # ve F # dahil) kullanılmak üzere tasarlanmıştır ve genellikle .NET nesne yönelimli stilini izler. Bu, işlevselliğin çoğunu yöntem içeren sınıflar (ve bazen genişletme yöntemleri veya statik yöntemler, ancak çoğunlukla kodun OO tasarımında yazılması gerekir) olarak göstereceğiniz anlamına gelir.

Sorunuzda, işlev bileşimini bir .NET kitaplığı olarak nasıl ortaya çıkaracağınızı soruyorsunuz, ancak sizin gibi işlevlerin compose.NET kitaplığı açısından çok düşük seviyeli kavramlar olduğunu düşünüyorum . Bunları Funcve ile çalışan yöntemler olarak gösterebilirsiniz Action, ancak bu muhtemelen ilk etapta normal bir .NET kitaplığını nasıl tasarlayacağınız değildir (belki bunun yerine Builder kalıbını veya bunun gibi bir şeyi kullanırsınız).

(Gerçekten NET kütüphane tarzı ile de uymaz sayısal kütüphaneler tasarlarken yani) Bazı durumlarda, bir kütüphane tasarımı için iyi bir mantıklı karışımlar hem F # ve .NET tek kütüphanede stilleri. Bunu yapmanın en iyi yolu, normal F # (veya normal .NET) API'sine sahip olmak ve ardından diğer stilde doğal kullanım için sarmalayıcılar sağlamaktır. Sarmalayıcılar ayrı bir ad alanında ( MyLibrary.FSharpve gibi MyLibrary) olabilir.

Örneğinizde, F # uygulamasını MyLibrary.FSharpiçeride bırakabilir ve ardından MyLibrarybazı sınıfların statik yöntemi olarak ad alanına .NET (C # dostu) sarmalayıcıları (Daniel'in gönderdiği koda benzer) ekleyebilirsiniz . Ancak yine, .NET kitaplığı muhtemelen işlev bileşiminden daha spesifik API'ye sahip olacaktır.


1
F # Bileşen Tasarım Yönergeleri artık burada
Jan Schiefer

19

Yalnızca işlev değerlerini (kısmen uygulanan işlevler vb.) FuncVeya ile sarmalısınız Action, geri kalanlar otomatik olarak dönüştürülür. Örneğin:

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

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

Bu yüzden Func/ uygun olan Actionyerlerde kullanmak mantıklıdır . Bu endişelerinizi ortadan kaldırıyor mu? Önerdiğiniz çözümlerin aşırı karmaşık olduğunu düşünüyorum. Tüm kitaplığınızı F # ile yazabilir ve F # ve C # ' dan ağrısız kullanabilirsiniz (bunu her zaman yaparım).

Ayrıca, F # birlikte çalışabilirlik açısından C # 'dan daha esnektir, bu nedenle bu bir sorun olduğunda genellikle geleneksel .NET stilini takip etmek en iyisidir.

DÜZENLE

İki genel arabirimi ayrı ad alanlarında yapmak için gereken çalışma, sanırım, yalnızca tamamlayıcı olduklarında veya F # işlevselliği C # 'dan kullanılamadığında garanti edilir (F #' ye özel meta verilere bağlı satır içi işlevler gibi).

Sırayla puanlarınızı almak:

  1. Modül + letbağlamaları ve yapıcısız tip + statik üyeler C # 'da tam olarak aynı görünür, bu yüzden yapabiliyorsanız modülleri kullanın. CompiledNameAttributeÜyelere C # dostu isimler vermek için kullanabilirsiniz .

  2. Yanılıyor olabilirim, ancak benim tahminim, Bileşen Kılavuzlarının System.Tupleçerçeveye eklenmeden önce yazılmış olduğu yönünde. (Daha önceki sürümlerde F #, kendi tuple tipini tanımladı.) O zamandan beri Tuple, önemsiz tipler için genel bir arayüzde kullanmak daha kabul edilebilir hale geldi .

  3. F # ile iyi oynadığı, Taskancak C # ile iyi oynamadığı için işleri C # yöntemiyle yaptığınızı düşündüğüm yer burasıdır Async. Zaman uyumsuzu dahili olarak kullanabilir, ardından Async.StartAsTaskgenel bir yöntemden dönmeden önce çağırabilirsiniz .

  4. nullC # 'dan kullanılmak üzere bir API geliştirirken en büyük dezavantajı kucaklamak olabilir. Geçmişte, dahili F # kodunda null olduğunu düşünmekten kaçınmak için her türlü püf noktasını denedim, ancak sonunda, en iyisi türleri public kurucularla işaretlemek [<AllowNullLiteral>]ve args'ı null için kontrol etmek en iyisiydi . Bu açıdan C # 'dan daha kötü değil.

  5. Ayrımcı sendikalar genellikle sınıf hiyerarşilerine göre derlenir, ancak C # 'da her zaman nispeten dostane bir temsile sahiptir. Onları işaretleyin [<AllowNullLiteral>]ve kullanın derdim .

  6. Curried işlevler , kullanılmaması gereken işlev değerleri üretir .

  7. Null kucaklamanın, genel arayüzde yakalanmasına bağlı olmaktan ve dahili olarak görmezden gelmekten daha iyi olduğunu buldum. YMMV.

  8. Dahili olarak list/ map/ kullanmak çok mantıklı set. Bunların tümü genel arayüz aracılığıyla gösterilebilir IEnumerable<_>. Ayrıca seq, dictve Seq.readonlysık sık yararlıdır.

  9. # 6'ya bakın.

Hangi stratejiyi uygulayacağınız, kitaplığınızın türüne ve boyutuna bağlıdır, ancak deneyimlerime göre, F # ve C # arasındaki tatlı noktayı bulmak, ayrı API'ler oluşturmaktan uzun vadede daha az iş gerektiriyordu.


evet, işleri yürütmenin bazı yolları olduğunu görebiliyorum, tamamen ikna olmadım. Re modülleri. Eğer varsa module Foo.Bar.ZooC # içinde bu class Fooad alanında olacaktır Foo.Bar. Ancak, iç içe geçmiş modülleriniz varsa module Foo=... module Bar=... module Zoo==, bu, Fooiç sınıflar Barve Zoo. Re IEnumerablebiz gerçekten haritalar ve setleri ile işe gerektiğinde, bu kabul olmayabilir. Re nulldeğerleri ve AllowNullLiteral- F # 'ı sevmemin bir nedeni null, C #' dan bıkmış olmamdır , gerçekten onunla her şeyi kirletmek istemem!
Philip P.

Birlikte çalışma için genellikle iç içe modüller yerine ad alanlarını tercih edin. Re: null, size katılıyorum, bu kesinlikle dikkate alınması gereken bir gerileme, ancak API'niz C #'dan kullanılacaksa uğraşmanız gereken bir şey.
Daniel

2

Muhtemelen aşırılık olsa da , IL düzeyinde dönüşümü otomatikleştirecek Mono.Cecil (posta listesinde harika bir desteğe sahiptir) kullanarak bir uygulama yazmayı düşünebilirsiniz . Örneğin, derlemenizi F # biçiminde, F # tarzı genel API kullanarak uygularsınız, ardından araç bunun üzerinde C # dostu bir sarmalayıcı oluşturur.

Örneğin, F # ' da C #' da olduğu gibi kullanmak yerine açıkça option<'T>( None, özellikle) kullanırsınız null. Bu senaryo için bir sarmalayıcı üreteci yazmak oldukça kolay olmalıdır: sarmalayıcı yöntemi orijinal yöntemi çağırır: eğer dönüş değeri olsaydı Some x, sonra geri dön x, aksi takdirde geri dön null.

TBir değer türü olduğunda, yani null yapılamaz durumda olduğunda durumu işlemeniz gerekir ; sarmalayıcı yönteminin dönüş değerini içine sarmalısınız Nullable<T>, bu da onu biraz acı verici hale getirir.

Yine, senaryonuzda böyle bir araç yazmanın karşılığını alacağından oldukça eminim, belki de bu tür kitaplık üzerinde düzenli olarak (her ikisi de F # ve C #'den sorunsuz olarak kullanılabilir) çalışmanız dışında. Her halükarda, bazen keşfedebileceğim ilginç bir deney olacağını düşünüyorum.


ilginç geliyor. Mono.CecilTemelde F # derlemesini yüklemek için bir program yazmak, eşleme gerektiren öğeleri bulmak ve bunları C # sarmalayıcılarla başka bir derleme yaymak için ortalama kullanmak anlamına gelir mi ? Bunu doğru anladım mı?
Philip P.

@Komrade P .: Bunu da yapabilirsiniz, ancak bu çok daha karmaşık, çünkü o zaman tüm referansları yeni meclis üyelerine işaret edecek şekilde ayarlamanız gerekir. Yeni üyeleri (sarmalayıcılar) mevcut F # ile yazılmış derlemeye eklerseniz çok daha kolay.
ShdNx

2

Taslak F # Bileşen Tasarım Rehberi (Ağustos 2010)

Genel Bakış Bu belge, F # bileşen tasarımı ve kodlamayla ilgili bazı konuları ele almaktadır. Özellikle şunları kapsar:

  • Herhangi bir .NET dilinden kullanılmak üzere "vanilla" .NET kitaplıkları tasarlama yönergeleri.
  • F # -to-F # kitaplıkları ve F # uygulama kodu için yönergeler.
  • F # uygulama kodu için kodlama kurallarına ilişkin öneriler
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.