ฉันกำลังพยายามออกแบบห้องสมุดใน 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 #! ตอนนี้ฉันต้องรวม F # มาตรฐานทั้งหมดfuns
ลงในFunc
และAction
ดังนี้:
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 # สำหรับทั้งผู้ใช้ F # และ C # ได้อย่างไร
จนถึงตอนนี้ฉันไม่สามารถหาอะไรได้ดีไปกว่าสองวิธีนี้:
- แอสเซมบลีสองชุดแยกกัน: ชุดหนึ่งกำหนดเป้าหมายไปยังผู้ใช้ F # และชุดที่สองสำหรับผู้ใช้ C #
- หนึ่งแอสเซมบลี แต่เนมสเปซต่างกัน: อันหนึ่งสำหรับผู้ใช้ F # และอันที่สองสำหรับผู้ใช้ C #
สำหรับแนวทางแรกฉันจะทำสิ่งนี้:
สร้างโครงการ F # เรียกว่า FooBarFs และรวบรวมเป็น FooBarFs.dll
- กำหนดเป้าหมายห้องสมุดเป็นผู้ใช้ F # เท่านั้น
- ซ่อนทุกสิ่งที่ไม่จำเป็นจากไฟล์. fsi
สร้างโปรเจ็กต์ F # อื่นเรียก FooBarCs และคอมไพล์ลงใน FooFar.dll
- นำโครงการ F # แรกมาใช้ซ้ำที่ระดับต้นทาง
- สร้างไฟล์. fsi ซึ่งซ่อนทุกอย่างจากโปรเจ็กต์นั้น
- สร้างไฟล์. fsi ซึ่งแสดงไลบรารีด้วยวิธี C # โดยใช้สำนวน C # สำหรับชื่อเนมสเปซ ฯลฯ
- สร้าง Wrapper ที่มอบสิทธิ์ให้กับไลบรารีหลักทำการแปลงเมื่อจำเป็น
ฉันคิดว่าแนวทางที่สองกับเนมสเปซอาจสร้างความสับสนให้กับผู้ใช้ แต่คุณมีแอสเซมบลีเดียว
คำถาม:สิ่งเหล่านี้ไม่เหมาะเลยบางทีฉันอาจพลาดแฟล็ก / สวิตช์ / แอตทริบิวต์ของคอมไพเลอร์หรือเคล็ดลับบางอย่างและมีวิธีที่ดีกว่าในการทำเช่นนี้หรือไม่
คำถาม:มีใครอีกบ้างที่พยายามบรรลุสิ่งที่คล้ายกันและถ้าเป็นเช่นนั้นคุณทำได้อย่างไร?
แก้ไข: เพื่อชี้แจงคำถามไม่เพียง แต่เกี่ยวกับฟังก์ชันและผู้รับมอบสิทธิ์เท่านั้น แต่ยังรวมถึงประสบการณ์โดยรวมของผู้ใช้ C # ที่มีไลบรารี F # ด้วย ซึ่งรวมถึงเนมสเปซรูปแบบการตั้งชื่อสำนวนและสิ่งที่คล้ายกันซึ่งเป็นภาษา C # โดยทั่วไปผู้ใช้ C # ไม่ควรตรวจพบว่าไลบรารีถูกเขียนขึ้นใน F # และในทางกลับกันผู้ใช้ F # ควรรู้สึกเหมือนกำลังจัดการกับไลบรารี C #
แก้ไข 2:
ฉันเห็นได้จากคำตอบและความคิดเห็นจนถึงตอนนี้ว่าคำถามของฉันไม่มีความลึกที่จำเป็นส่วนใหญ่อาจเกิดจากการใช้เพียงตัวอย่างเดียวที่ปัญหาการทำงานร่วมกันระหว่าง F # และ C # เกิดขึ้นปัญหาของค่าฟังก์ชัน ฉันคิดว่านี่เป็นตัวอย่างที่ชัดเจนที่สุดดังนั้นสิ่งนี้ทำให้ฉันใช้มันเพื่อถามคำถาม แต่ด้วยโทเค็นเดียวกันทำให้รู้สึกว่านี่เป็นปัญหาเดียวที่ฉันกังวล
ให้ฉันยกตัวอย่างที่เป็นรูปธรรมมากขึ้น ฉันได้อ่าน เอกสารแนวทางการออกแบบส่วนประกอบ F # ที่ยอดเยี่ยมที่สุดแล้ว (ขอบคุณมาก @gradbot สำหรับสิ่งนี้!) หลักเกณฑ์ในเอกสารหากใช้จะช่วยแก้ปัญหาบางประการ แต่ไม่ใช่ทั้งหมด
เอกสารนี้แบ่งออกเป็นสองส่วนหลัก: 1) แนวทางในการกำหนดเป้าหมายผู้ใช้ F #; และ 2) แนวทางในการกำหนดเป้าหมายผู้ใช้ C # ไม่มีที่ไหนแม้แต่จะพยายามแสร้งทำเป็นว่าเป็นไปได้ที่จะมีแนวทางที่เหมือนกันซึ่งสะท้อนคำถามของฉันอย่างแน่นอน: เราสามารถกำหนดเป้าหมาย F # เราสามารถกำหนดเป้าหมาย C # ได้ แต่วิธีแก้ปัญหาที่ใช้ได้จริงสำหรับการกำหนดเป้าหมายทั้งสองคืออะไร?
เพื่อเป็นการเตือนความจำเป้าหมายคือการมีห้องสมุดที่เขียนใน F # และสามารถใช้งานได้จากทั้งภาษา F # และ C #
คำหลักที่นี่คือสำนวน ปัญหาไม่ได้อยู่ที่ความสามารถในการทำงานร่วมกันโดยทั่วไปซึ่งเป็นไปได้ที่จะใช้ไลบรารีในภาษาต่างๆ
ตอนนี้เป็นตัวอย่างซึ่งฉันนำมาจาก แนวทางการออกแบบส่วนประกอบ F #โดยตรง
โมดูล + ฟังก์ชัน (F #) เทียบกับ Namespaces + ประเภท + ฟังก์ชัน
F #: ใช้เนมสเปซหรือโมดูลเพื่อบรรจุประเภทและโมดูลของคุณ การใช้สำนวนคือการวางฟังก์ชั่นในโมดูลเช่น:
// library module Foo let bar() = ... let zoo() = ... // Use from F# open Foo bar() zoo()
C #: ใช้เนมสเปซประเภทและสมาชิกเป็นโครงสร้างองค์กรหลักสำหรับส่วนประกอบของคุณ (ซึ่งตรงข้ามกับโมดูล) สำหรับ vanilla .NET API
สิ่งนี้ไม่เข้ากันกับแนวทาง F # และตัวอย่างจะต้องถูกเขียนใหม่เพื่อให้เหมาะกับผู้ใช้ C #:
[<AbstractClass; Sealed>] type Foo = static member bar() = ... static member zoo() = ...
โดยการทำเช่นแม้ว่าเราจะแบ่งการใช้สำนวนจาก F # เพราะเราสามารถที่ไม่ได้ใช้
bar
และzoo
โดยไม่ต้อง prefixingFoo
ด้วย
การใช้ tuples
F #: ใช้ tuples เมื่อเหมาะสมกับค่าที่ส่งคืน
C #: หลีกเลี่ยงการใช้ tuples เป็นค่าส่งคืนใน vanilla .NET API
async
F #: ใช้ Async สำหรับการเขียนโปรแกรม async ที่ขอบเขต F # API
C #: แสดงการดำเนินการแบบอะซิงโครนัสโดยใช้รูปแบบการเขียนโปรแกรมแบบอะซิงโครนัส. NET (BeginFoo, EndFoo) หรือเป็นวิธีการที่ส่งคืนงาน. NET (งาน) แทนที่จะเป็นวัตถุ F # Async
การใช้
Option
F #: พิจารณาใช้ค่าตัวเลือกสำหรับประเภทการส่งคืนแทนการเพิ่มข้อยกเว้น (สำหรับรหัส F # -facing)
ลองใช้รูปแบบ TryGetValue แทนการส่งคืนค่าอ็อพชัน F # (ตัวเลือก) ใน vanilla .NET APIs และชอบที่วิธีการโอเวอร์โหลดมากกว่าการใช้ค่าอ็อพชัน F # เป็นอาร์กิวเมนต์
สหภาพแรงงานที่เลือกปฏิบัติ
F #: ใช้สหภาพแรงงานที่เลือกปฏิบัติเป็นทางเลือกแทนลำดับชั้นของคลาสสำหรับการสร้างข้อมูลที่มีโครงสร้างแบบต้นไม้
C #: ไม่มีแนวทางเฉพาะสำหรับเรื่องนี้ แต่แนวคิดของสหภาพแรงงานที่เลือกปฏิบัตินั้นต่างจาก C #
ฟังก์ชัน Curried
F #: ฟังก์ชัน curried เป็นสำนวนสำหรับ F #
C #: อย่าใช้การแกงของพารามิเตอร์ใน vanilla .NET API
กำลังตรวจสอบค่าว่าง
F #: นี่ไม่ใช่สำนวนสำหรับ F #
C #: พิจารณาตรวจสอบค่า null บนขอบเขต vanilla .NET API
การใช้ F # ประเภท
list
,map
,set
ฯลฯF #: มันเป็นสำนวนที่จะใช้สิ่งเหล่านี้ใน F #
C #: พิจารณาใช้อินเทอร์เฟซการรวบรวม. NET ประเภท IEnumerable และ IDictionary สำหรับพารามิเตอร์และส่งคืนค่าใน vanilla .NET APIs ( คือไม่ได้ใช้ F #
list
,map
,set
)
ประเภทฟังก์ชัน (ที่ชัดเจน)
F #: การใช้ฟังก์ชัน F # เนื่องจากค่าเป็นสำนวนสำหรับ F # อย่างชัดเจน
C #: ใช้ประเภทผู้ร่วมประชุม. NET ตามความต้องการของประเภทฟังก์ชัน F # ใน vanilla .NET API
ฉันคิดว่าสิ่งเหล่านี้น่าจะเพียงพอที่จะแสดงให้เห็นถึงลักษณะของคำถามของฉัน
อนึ่งหลักเกณฑ์ยังมีคำตอบบางส่วน:
... กลยุทธ์การใช้งานทั่วไปเมื่อพัฒนาวิธีการลำดับที่สูงขึ้นสำหรับไลบรารี vanilla .NET คือการสร้างการใช้งานทั้งหมดโดยใช้ประเภทฟังก์ชัน F # จากนั้นสร้าง API สาธารณะโดยใช้ผู้รับมอบสิทธิ์เป็นส่วนหน้าบาง ๆ บนการใช้งาน F # จริง
เพื่อสรุป
มีหนึ่งคำตอบที่ชัดเจนคือไม่มีเทคนิคคอมไพเลอร์ที่ไม่ได้รับ
ตามแนวทางเอกสารดูเหมือนว่าการเขียน F # ก่อนแล้วจึงสร้างกระดาษห่อหุ้มซุ้มสำหรับ. NET เป็นกลยุทธ์ที่สมเหตุสมผล
คำถามยังคงอยู่เกี่ยวกับการนำไปใช้จริง:
แยกประกอบ? หรือ
เนมสเปซต่างกัน?
หากการตีความของฉันถูกต้องโทมัสแนะนำว่าการใช้เนมสเปซแยกต่างหากควรจะเพียงพอและควรเป็นวิธีแก้ปัญหาที่ยอมรับได้
ฉันคิดว่าฉันจะเห็นด้วยกับเรื่องนี้เนื่องจากการเลือกเนมสเปซนั้นไม่แปลกใจหรือสับสนกับผู้ใช้. NET / C # ซึ่งหมายความว่าเนมสเปซสำหรับพวกเขาควรมีลักษณะเป็นเนมสเปซหลักสำหรับพวกเขา ผู้ใช้ F # จะต้องรับภาระในการเลือก F # - เฉพาะเนมสเปซ ตัวอย่างเช่น:
FSharp.Foo.Bar -> เนมสเปซสำหรับ F # หันหน้าไปทางไลบรารี
Foo.Bar -> เนมสเปซสำหรับ. NET wrapper สำนวนสำหรับ C #