
يسرنا أن نعلن أن Visual Studio 2019 سوف يشحن نسخة جديدة من F # عندما يصدر: F # 4.6!
F # 4.6 هو تحديث أصغر للغة F # ، مما يجعله إصدارًا "حقيقيًا" للنقاط. كما هو الحال مع الإصدارات السابقة من F # ، تم تطوير F # 4.6 بالكامل عبر عملية RFC مفتوحة (طلبات للتعليقات). قدم مجتمع F # تعليقات مفصلة للغاية في المناقشات الخاصة بهذا الإصدار من اللغة. يمكنك عرض جميع طلبات RFC التي تتوافق مع هذا الإصدار هنا:
سوف يعرض هذا المنشور تفاصيل مجموعة الميزات وكيفية البدء.
الأصل في بلوقابدأ
أولاً ، قم بتثبيت إما:
بعد ذلك ، قم بتحديث تبعية FSharp.Core الخاصة بك إلى FSharp.Core 4.6 (أو أعلى). إذا كنت تستخدم Visual Studio ، فيمكنك القيام بذلك باستخدام واجهة المستخدم لإدارة حزمة NuGet. إذا كنت لا تستخدم Visual Studio ، أو تفضل ملفات مشروع التحرير اليدوي ، فأضف ذلك إلى ملف المشروع:
<ItemGroup> <PackageReference Update="FSharp.Core" Version="4.6.0" /> </ItemGroup>
بمجرد تثبيت البتات الضرورية ، يمكنك استخدام F # 4.6 مع Visual Studio أو Visual Studio for Mac أو Visual Studio Code مع Ionide .
سجلات مجهولة
بصرف النظر عن إصلاحات الأخطاء المختلفة ، فإن التغيير اللغوي الوحيد في F # 4.6 هو إدخال أنواع سجلات مجهولة .
الاستخدام الأساسي
من منظور F # فقط ، تعد السجلات المجهولة أنواع سجلات F # التي لا تحتوي على أسماء للشرح ويمكن إعلانها في صورة مؤقتة. على الرغم من أنه من غير المحتمل أن يقوموا بتغيير طريقة كتابة رمز F # بشكل أساسي ، إلا أنهم يملأون العديد من الفجوات الأصغر التي واجهها مبرمجو F # بمرور الوقت ، ويمكن استخدامها لمعالجة البيانات المختصرة التي لم تكن ممكنة من قبل.
إنها سهلة الاستخدام. على سبيل المثال ، هنا كيف يمكنك التفاعل مع دالة تنتج سجلًا مجهولًا:
open System let circleStats radius = let d = radius * 2.0 let a = Math.PI * (radius ** 2.0) let c = 2.0 * Math.PI * radius {| Diameter=d; Area=a; Circumference=c |} let r = 2.0 let stats = circleStats r printfn "Circle with radius: %f has diameter %f, area %f, and circumference %f" r stats.Diameter stats.Area stats.Circumference
ومع ذلك ، يمكن استخدامها لأكثر من حاويات البيانات الأساسية فقط. يوسع النموذج التالي العينة السابقة لاستخدام وظيفة طباعة أكثر أمانًا في الكتابة:
let circleStats radius = let d = radius * 2.0 let a = Math.PI * (radius ** 2.0) let c = 2.0 * Math.PI * radius {| Diameter=d; Area=a; Circumference=c |} let printCircleStats r (stats: {| Area: float; Circumference: float; Diameter: float |}) = printfn "Circle with radius: %f has diameter %f, area %f, and circumference %f" r stats.Diameter stats.Area stats.Circumference let r = 2.0 let stats = circleStats r printCircleStats r stats
إذا حاولت استدعاء `printCircleStats` بسجل مجهول له نفس أنواع البيانات الأساسية ولكن مع تسميات مختلفة ، فسيفشل في الترجمة:
printCircleStats r {| Diameter=2.0; Area=4.0; MyCircumference=12.566371 |}
هذا هو بالضبط الطريقة التي تعمل بها أنواع سجلات F # ، باستثناء أن كل شيء تم إعلانه بشكل مخصص وليس مقدمًا. هذا له فوائد وعيوب تبعًا للموقف الخاص بك ، لذلك نوصي باستخدام سجلات مجهولة بحكمة بدلاً من استبدال جميع إعلانات السجل F # المقدمة.
بناء سجلات مجهولة
يمكن أن تكون السجلات المجهولة أيضًا بنيات باستخدام الكلمة الأساسية للبنية :
open System let circleStats radius = let d = radius * 2.0 let a = Math.PI * (radius ** 2.0) let c = 2.0 * Math.PI * radius
يمكنك استدعاء وظيفة تأخذ سجل هيكلي مجهول يمكن القيام به بوضوح مثل هذا:
let printCircleStats r (stats: struct {| Area: float; Circumference: float; Diameter: float |}) = printfn "Circle with radius: %f has diameter %f, area %f, and circumference %f" r stats.Diameter stats.Area stats.Circumference printCircleStats r struct {| Area=4.0; Circumference=12.6; Diameter=12.6 |}
أو يمكنك استخدام "الاستدلال على البنية" لإلغاء تحديد "البنية" في موقع الاتصال:
let printCircleStats r (stats: struct {| Area: float; Circumference: float; Diameter: float |}) = printfn "Circle with radius: %f has diameter %f, area %f, and circumference %f" r stats.Diameter stats.Area stats.Circumference printCircleStats r {| Area=4.0; Circumference=12.6; Diameter=12.6 |}
سيعامل هذا مثيل السجل المجهول الذي قمت بإنشائه كما لو كان بنية.
لاحظ أن العكس ليس صحيحًا:
let printCircleStats r (stats: {| Area: float; Circumference: float; Diameter: float |}) = printfn "Circle with radius: %f has diameter %f, area %f, and circumference %f" r stats.Diameter stats.Area stats.Circumference
لا يمكن حاليًا تحديد IsByRefLike أو IsReadOnly لتكوين أنواع سجلات مجهولة. هناك اقتراح لغوي يقترح هذا التحسين ، ولكن نظرًا لوجود بعض الغرائب في بناء الجملة ، لا يزال قيد المناقشة.
أخذ الأمور أكثر
يمكن استخدام السجلات المجهولة في مجموعة أوسع من السياقات المتقدمة.
سجلات مجهولة قابلة للتسلسل
يمكنك إجراء تسلسل وإلغاء تسجيل السجلات المجهولة:
open Newtonsoft.Json let phillip = {| name="Phillip"; age=28 |} let str = JsonConvert.SerializeObject(phillip) printfn "%s" str let phillip' = JsonConvert.DeserializeObject<{|name: string; age: int|}>(str) printfn "Name: %s Age: %d" phillip'.name phillip'.age
هذا يخرج ما قد تتوقعه:
{"age":28,"name":"Phillip"} Name: Phillip Age: 28
فيما يلي مكتبة نماذج تسمى أيضًا في مشروع آخر:
namespace AnonyRecdOne open Newtonsoft.Json module AR = let serialize () = let phillip = {| name="Phillip"; age=28 |} JsonConvert.SerializeObject(phillip)
open AnonyRecdOne open Newtonsoft.Json [<EntryPoint>] let main _ = let str = AR.serialize () let phillip = JsonConvert.DeserializeObject<{|name: string; age: int|}>(str) printfn "Name: %s Age: %d" phillip.name phillip.age
هذا قد يسهل الأمور على السيناريوهات مثل البيانات خفيفة الوزن التي تمر عبر شبكة في نظام مكون من خدمات microservices.
يمكن دمج السجلات المجهولة مع تعريفات الأنواع الأخرى
قد يكون لديك نموذج بيانات شبيه بالشجرة في نطاقك ، مثل المثال التالي:
type FullName = { FirstName: string; LastName: string } type Employee = | Engineer of FullName | Manager of name: FullName * reports: Employee list | Executive of name: FullName * reports: Employee list * assistant: Employee
من المعتاد أن ترى الحالات المصممة على أنها tuples مع حقول الاتحاد المسماة ، ولكن مع زيادة تعقيد البيانات ، يمكنك استخراج كل حالة من السجلات:
type FullName = { FirstName: string; LastName: string } type Employee = | Engineer of FullName | Manager of Manager | Executive of Executive and Manager = { Name: FullName; Reports: Employee list } and Executive = { Name: FullName; Reports: Employee list; Assistant: Employee }
يمكن الآن اختصار هذا التعريف المتكرر بسجلات مجهولة إذا كان يناسب قاعدة البيانات الخاصة بك:
type FullName = { FirstName: string; LastName: string } type Employee = | Engineer of FullName | Manager of {| Name: FullName; Reports: Employee list |} | Executive of {| Name: FullName; Reports: Employee list; Assistant: Employee |}
كما في الأمثلة السابقة ، يجب تطبيق هذه التقنية بحكمة وعند الاقتضاء على السيناريو الخاص بك.
السجلات مجهولة المصدر تسهيل استخدام LINQ في F #
عادةً ما يفضل مبرمجو F # استخدام توليفات List و Array و Sequence عند العمل مع البيانات ، لكن قد يكون من المفيد في بعض الأحيان استخدام LINQ . كان هذا تقليديًا مؤلمًا بعض الشيء ، حيث تستخدم LINQ أنواع C # مجهولة.
باستخدام السجلات المجهولة ، يمكنك استخدام أساليب LINQ تمامًا كما تفعل مع C # وأنواع مجهولة:
open System.Linq let names = [ "Ana"; "Felipe"; "Emillia"] let nameGrouping = names.Select(fun n -> {| Name=n; FirstLetter=n.[0] |}) for ng in nameGrouping do printfn "%s has first letter %c" ng.Name ng.FirstLetter
هذه المطبوعات:
Ana has first letter A Felipe has first letter F Emillia has first letter E
تسهل السجلات المجهولة العمل مع Entity Framework و ORMs الأخرى
يجب أن يشاهد مبرمجو F # الذين يستخدمون تعبيرات استعلام F # للتفاعل مع قاعدة بيانات بعض التحسينات الطفيفة في جودة الحياة باستخدام سجلات مجهولة.
على سبيل المثال ، قد تستخدم في استخدام tuples لتجميع البيانات باستخدام جملة "select":
let q = query { for row in db.Status do select (row.StatusID, row.Name) }
لكن ينتج عن ذلك أعمدة بأسماء مثل Item1 و Item2 ليست مثالية. قبل السجلات المجهولة ، ستحتاج إلى الإعلان عن نوع السجل واستخدام ذلك. الآن لا تحتاج إلى القيام بذلك:
let q = query { for row in db.Status do select {| StatusID = row.StatusID; Name = row.Name |} }
لا تحتاج إلى تحديد نوع السجل مقدما! هذا يجعل تعبيرات الاستعلام أكثر توافقًا مع SQL الفعلي الذي يصممونه.
تتيح لك السجلات المجهولة أيضًا تجنب الاضطرار إلى إنشاء أنواع AnonymousObject في استعلامات أكثر تقدمًا فقط لإنشاء مجموعة بيانات مخصصة لأغراض الاستعلام.
تعمل السجلات المجهولة على تسهيل استخدام التوجيه المخصص في ASP.NET Core
ربما كنت تستخدم ASP.NET Core مع F # بالفعل ، ولكن ربما واجهت صعوبة عند تحديد المسارات المخصصة. كما هو الحال مع الأمثلة السابقة ، لا يزال من الممكن القيام بذلك عن طريق تحديد نوع السجل مقدمًا ، ولكن غالبًا ما كان مطورو F # غير ضروريين. الآن يمكنك أن تفعل ذلك مضمنة:
app.UseMvc(fun routes -> routes.MapRoute("blog","blog/{*article}", defaults={| controller="Blog"; action="Article" |}) |> ignore ) |> ignore
لا يزال هذا غير مثالي نظرًا لحقيقة أن F # صارمة بشأن أنواع الإرجاع (على عكس C # ، حيث لا تحتاج إلى تجاهل الأشياء التي تُرجع قيمة بشكل صريح). ومع ذلك ، يتيح لك ذلك إزالة تعريفات السجل المعرفة مسبقًا والتي لا تخدم أي غرض سوى السماح لك بإرسال البيانات إلى خط أنابيب الوسيطة ASP.NET.
نسخ وتحديث التعبيرات مع سجلات مجهولة
كما هو الحال مع أنواع السجلات ، يمكنك استخدام بناء جملة النسخ والتحديث مع سجلات مجهولة:
let data = {| X = 1; Y = 2 |} let expandedData = {| data with Z = 3 |}
يمكن أن يكون التعبير الأصلي أيضًا نوع سجل:
type R = { X: int } let data = { X=1 } let data' = {| data with Y = 2 |}
يمكنك أيضًا نسخ البيانات من وإلى السجلات المرجعية والمجهولة:
يتيح استخدام تعبيرات النسخ والتحديث للسجلات المجهولة درجة عالية من المرونة عند التعامل مع البيانات الموجودة في F #.
المساواة ونمط مطابقة
السجلات المجهولة متساوية هيكلياً وقابلة للمقارنة:
{| a = 1+1 |} = {| a = 2 |}
ومع ذلك ، يجب أن يكون للأنواع التي تتم مقارنتها نفس الشكل
على الرغم من أنه يمكنك مساواة ومقارنة السجلات المجهولة ، إلا أنه لا يمكنك مطابقة النقش عليها. هذا لسببين:
- يجب أن يمثل النمط حسابًا لكل حقل في سجل مجهول ، على عكس أنواع السجلات. وذلك لأن السجلات المجهولة لا تدعم النمط الفرعي الهيكلي - فهي أنواع اسمية.
- لا توجد قدرة على الحصول على أنماط إضافية في تعبير مطابقة النقش ، حيث أن كل نمط مميز ينطوي على نوع مختلف من السجلات المجهولة.
- إن شرط حساب كل حقل في سجل مجهول من شأنه أن يجعل نمطًا أكثر مطوَّلاً من استخدام تدوين "النقطة".
بدلاً من ذلك ، يتم استخدام "dot" -syntax لاستخراج القيم من سجل مجهول. سيكون هذا دائمًا على الأكثر مطولًا كما لو تم استخدام مطابقة الأنماط ، ومن المرجح في الممارسة العملية أن يكون أقل مطولًا بسبب عدم استخراج كل قيمة دائمًا من سجل مجهول. في ما يلي كيفية التعامل مع مثال سابق حيث تكون السجلات المجهولة جزءًا من اتحاد متمايز:
type Employee = | Engineer of FullName | Manager of {| Name: FullName; Reports: Employee list |} | Executive of {| Name: FullName; Reports: Employee list; Assistant: Employee |} let getFirstName e = match e with | Engineer fullName -> fullName.FirstName | Manager m -> m.Name.FirstName | Executive ex -> ex.Name.FirstName
يوجد حاليًا اقتراح مفتوح للسماح بمطابقة الأنماط في سجلات مجهولة في السياقات المحدودة التي يمكن تمكينها بالفعل. إذا كان لديك حالة استخدام مقترحة ، فيرجى استخدام هذه المشكلة لمناقشتها!
FSharp.Core الإضافات
لن يكون إصدار F # آخر بدون إضافات إلى مكتبة F # Core!
توسيع القيمة
يقدم نوع ValueOption في F # 4.5 الآن عددًا قليلاً من الأشياء الجيدة المرتبطة بالنوع:
- سمة DebuggerDisplay للمساعدة في تصحيح الأخطاء
- أعضاء IsNone و IsSome و None و Some و op_Implicit و ToString
هذا يعطيه "التكافؤ" مع نوع الخيار.
بالإضافة إلى ذلك ، هناك الآن وحدة ValueOption تحتوي على نفس الوظائف التي تحتوي عليها وحدة `Option`:
module ValueOption = [<CompiledName("IsSome")>] val inline isSome: voption:'T voption -> bool [<CompiledName("IsNone")>] val inline isNone: voption:'T voption -> bool [<CompiledName("DefaultValue")>] val defaultValue: value:'T -> voption:'T voption -> 'T [<CompiledName("DefaultWith")>] val defaultWith: defThunk:(unit -> 'T) -> voption:'T voption -> 'T [<CompiledName("OrElse")>] val orElse: ifNone:'T voption -> voption:'T voption -> 'T voption [<CompiledName("OrElseWith")>] val orElseWith: ifNoneThunk:(unit -> 'T voption) -> voption:'T voption -> 'T voption [<CompiledName("GetValue")>] val get: voption:'T voption -> 'T [<CompiledName("Count")>] val count: voption:'T voption -> int [<CompiledName("Fold")>] val fold<'T,'State> : folder:('State -> 'T -> 'State) -> state:'State -> voption:'T voption -> 'State [<CompiledName("FoldBack")>] val foldBack<'T,'State> : folder:('T -> 'State -> 'State) -> voption:'T voption -> state:'State -> 'State [<CompiledName("Exists")>] val exists: predicate:('T -> bool) -> voption:'T voption -> bool [<CompiledName("ForAll")>] val forall: predicate:('T -> bool) -> voption:'T voption -> bool [<CompiledName("Contains")>] val inline contains: value:'T -> voption:'T voption -> bool when 'T : equality [<CompiledName("Iterate")>] val iter: action:('T -> unit) -> voption:'T voption -> unit [<CompiledName("Map")>] val map: mapping:('T -> 'U) -> voption:'T voption -> 'U voption [<CompiledName("Map2")>] val map2: mapping:('T1 -> 'T2 -> 'U) -> voption1: 'T1 voption -> voption2: 'T2 voption -> 'U voption [<CompiledName("Map3")>] val map3: mapping:('T1 -> 'T2 -> 'T3 -> 'U) -> 'T1 voption -> 'T2 voption -> 'T3 voption -> 'U voption [<CompiledName("Bind")>] val bind: binder:('T -> 'U voption) -> voption:'T voption -> 'U voption [<CompiledName("Flatten")>] val flatten: voption:'T voption voption -> 'T voption [<CompiledName("Filter")>] val filter: predicate:('T -> bool) -> voption:'T voption -> 'T voption [<CompiledName("ToArray")>] val toArray: voption:'T voption -> 'T[] [<CompiledName("ToList")>] val toList: voption:'T voption -> 'T list [<CompiledName("ToNullable")>] val toNullable: voption:'T voption -> Nullable<'T> [<CompiledName("OfNullable")>] val ofNullable: value:Nullable<'T> -> 'T voption [<CompiledName("OfObj")>] val ofObj: value: 'T -> 'T voption when 'T : null [<CompiledName("ToObj")>] val toObj: value: 'T voption -> 'T when 'T : null
هذا يجب أن يخفف من أي مخاوف من أن "ValueOption" هو الأخ غير الشقيق لـ "Option" والذي لا يحصل على نفس مجموعة الوظائف.
tryExactlyOne لـ List و Array و Seq
هذه الوظيفة الرائعة ساهمت بها Grzegorz Dziadkiewicz . إليك كيف تعمل:
List.tryExactlyOne []
اختتام
على الرغم من أن القائمة الإجمالية للميزات في F # 4.6 ليست ضخمة ، فإنها لا تزال عميقة للغاية! نحن نشجعك على تجربة F # 4.6 وترك لنا ردود الفعل حتى نتمكن من صقل الأشياء قبل الإصدار الكامل. كما هو الحال دائمًا ، نشكرك إلى مجتمع F # على مساهماتهم - سواء في مناقشة التعليمات البرمجية أو التصميم - التي تساعدنا على مواصلة تطوير لغة F #.
هتاف ، وسرقة القرصنة!