2016-03-10 18 views
11

Tôi đã theo dõi this F# ROP article và quyết định thử tái sản xuất trong C#, chủ yếu để xem liệu tôi có thể. Xin lỗi vì chiều dài của câu hỏi này, nhưng nếu bạn quen thuộc với ROP, nó sẽ rất dễ làm theo.Lập trình hướng tuyến đường trong C# - Làm cách nào để viết chức năng chuyển đổi?

Ông bắt đầu với một F # phân biệt đối xử công đoàn ...

type Result<'TSuccess, 'TFailure> = 
    | Success of 'TSuccess 
    | Failure of 'TFailure 

... mà tôi dịch sang một lớp RopValue trừu tượng, và hai triển khai cụ thể (lưu ý rằng tôi đã thay đổi tên lớp cho những người thân mà Tôi hiểu rõ hơn) ...

public abstract class RopValue<TSuccess, TFailure> { 
    public static RopValue<TSuccess, TFailure> Create<TSuccess, TFailure>(TSuccess input) { 
    return new Success<TSuccess, TFailure>(input); 
    } 
} 

public class Success<TSuccess, TFailure> : RopValue<TSuccess, TFailure> { 
    public Success(TSuccess value) { 
    Value = value; 
    } 
    public TSuccess Value { get; set; } 
} 

public class Failure<TSuccess, TFailure> : RopValue<TSuccess, TFailure> { 
    public Failure(TFailure value) { 
    Value = value; 
    } 
    public TFailure Value { get; set; } 
} 

Tôi đã thêm phương pháp tạo tĩnh để cho phép bạn tạo RopValue từ đối tượng TSuccess, được đưa vào đầu tiên của hàm xác thực.

Sau đó, tôi đã viết về chức năng liên kết. Phiên bản F # như sau ...

let bind switchFunction twoTrackInput = 
    match twoTrackInput with 
    | Success s -> switchFunction s 
    | Failure f -> Failure f 

... đó là một doddle để đọc so với C# tương đương! Tôi không biết nếu có một cách đơn giản hơn để viết những dòng này, nhưng đây là những gì tôi đã đưa ra ...

public static RopValue<TSuccess, TFailure> Bind<TSuccess, TFailure>(this RopValue<TSuccess, TFailure> input, Func<RopValue<TSuccess, TFailure>, RopValue<TSuccess, TFailure>> switchFunction) { 
    if (input is Success<TSuccess, TFailure>) { 
    return switchFunction(input); 
    } 
    return input; 
} 

Lưu ý rằng tôi đã viết này như là một chức năng mở rộng, vì điều đó cho phép tôi sử dụng nó một cách chức năng hơn.

Lấy trường hợp sử dụng của ông xác nhận một người, sau đó tôi đã viết một lớp người ...

public class Person { 
    public string Name { get; set; } 
    public string Email { get; set; } 
    public int Age { get; set; } 
} 

... và viết chức năng xác nhận đầu tiên của tôi ...

public static RopValue<Person, string> CheckName(RopValue<Person, string> res) { 
    if (res.IsSuccess()) { 
    Person person = ((Success<Person, string>)res).Value; 
    if (string.IsNullOrWhiteSpace(person.Name)) { 
     return new Failure<Person, string>("No name"); 
    } 
    return res; 
    } 
    return res; 
} 

Với một một vài xác thực tương tự cho email và tuổi, tôi có thể viết chức năng xác nhận tổng thể như sau ...

private static RopValue<Person, string> Validate(Person person) { 
    return RopValue<Person, string> 
    .Create<Person, string>(person) 
    .Bind(CheckName) 
    .Bind(CheckEmail) 
    .Bind(CheckAge); 
} 

Điều này làm việc tốt, và cho phép tôi làm một cái gì đó như thế này ...

Person jim = new Person {Name = "Jim", Email = "", Age = 16}; 
RopValue<Person, string> jimChk = Validate(jim); 
Debug.WriteLine("Jim returned: " + (jimChk.IsSuccess() ? "Success" : "Failure")); 

Tuy nhiên, tôi có một vài vấn đề với cách tôi đã làm điều này. Trước hết là các chức năng xác nhận yêu cầu bạn phải vượt qua trong RopValue, kiểm tra nó để thành công hay thất bại, nếu thành công, kéo ra khỏi Person và sau đó xác nhận nó. Nếu thất bại, chỉ cần trả lại.

Ngược lại, chức năng xác nhận của ông mất (tương đương) một người, và trả lại (một quả, đó là tương đương) một RopValue ...

let validateNameNotBlank person = 
    if person.Name = "" then Failure "Name must not be blank" 
    else Success person 

Đây là đơn giản hơn nhiều, nhưng tôi không thể tìm ra cách thực hiện điều này trong C#.

Một vấn đề khác là chúng tôi bắt đầu chuỗi xác nhận với thành công <>, vì vậy hàm xác thực đầu tiên sẽ luôn trả về thứ gì đó từ khối "if", hoặc thất bại <> nếu xác thực không thành công hoặc thành công <> nếu chúng tôi vượt qua kiểm tra. Nếu một hàm trả về Failure <>, thì hàm tiếp theo trong chuỗi xác nhận không bao giờ được gọi, vì vậy nó chỉ ra rằng chúng ta biết rằng các phương thức này không bao giờ có thể được thông qua một Failure <>.Vì vậy, dòng cuối cùng của mỗi chức năng này không bao giờ có thể đạt được (khác với trong trường hợp kỳ lạ mà bạn tự tạo ra một thất bại <> và thông qua nó trong lúc bắt đầu, nhưng đó sẽ là vô nghĩa).

Sau đó, ông đã tạo toán tử chuyển đổi (> =>) để kết nối các chức năng xác thực. Tôi đã thử làm điều này, nhưng không thể làm cho nó hoạt động được. Để chuỗi các cuộc gọi liên tiếp đến hàm, có vẻ như tôi phải có một phương thức mở rộng trên một Func <>, mà tôi không nghĩ rằng bạn có thể làm. Tôi đã đạt được điều này ...

public static RopValue<TSuccess, TFailure> Switch<TSuccess, TFailure>(this Func<TSuccess, RopValue<TSuccess, TFailure>> switch1, Func<TSuccess, RopValue<TSuccess, TFailure>> switch2, TSuccess input) { 
    RopValue<TSuccess, TFailure> res1 = switch1(input); 
    if (res1.IsSuccess()) { 
    return switch2(((Success<TSuccess, TFailure>)res1).Value); 
    } 
    return new Failure<TSuccess, TFailure>(((Failure<TSuccess, TFailure>)res1).Value); 
} 

... nhưng không thể tìm cách sử dụng.

Vì vậy, bất kỳ ai cũng có thể giải thích cách tôi sẽ viết hàm Bind để có thể lấy một Người và trả lại RopValue (giống như của anh ta) không? Ngoài ra làm cách nào để viết chức năng chuyển đổi cho phép tôi kết nối các chức năng xác thực đơn giản?

Bất kỳ nhận xét nào khác về mã của tôi đều được chào đón. Tôi không chắc nó ở đâu gần như gọn gàng và đơn giản như nó có thể được.

+0

Tất cả điều này đánh tôi giống như phần 'Lỗi ' đơn nguyên. Bạn có cảm thấy đó là nhưng với loại 'TFailure' có hiệu quả tương đương với' Ngoại lệ '? – Enigmativity

+0

@Enigmativity Để trích dẫn Mark Twain, "Tôi rất vui khi có thể trả lời nhanh, tôi nói tôi không biết!" - Tôi là lập trình viên C#, học lập trình F # và lập trình hàm cùng một lúc. Tôi đã nhìn thấy các monads được thảo luận, nhưng vẫn chưa biết được chúng là gì. –

+0

Loại 'RopValue' của bạn là một đơn nguyên. Về cơ bản monads cung cấp cho các loại bình thường siêu quyền hạn. – Enigmativity

Trả lời

4

chức năng Bind của bạn có gõ sai, nó phải là:

public static RopValue<TOut, TFailure> Bind<TSuccess, TFailure, TOut>(this RopValue<TSuccess, TFailure> input, Func<TSuccess, RopValue<TOut, TFailure>> switchFunction) { 
    if (input is Success<TSuccess, TFailure>) { 
    return switchFunction(((Success<TSuccess, TFailure>)input).Value); 
    } 
    return new Failure<TOut, TFailure>(((Failure<TSuccess, TFailure>)input).Value); 
} 

Tham số Func truyền cho thực hiện lại Bind mất một tham số RopValue<TSuccess, TFailure> thay vì chỉ TSuccess. Điều này có nghĩa là hàm cần lặp lại cùng một kết hợp trên đầu vào mà phương thức Bind sẽ làm cho bạn.

Đây có thể là một chút khó sử dụng do số lượng các tham số kiểu để bạn có thể di chuyển nó đến lớp cơ sở:

public abstract RopValue<TOut, TFailure> Bind<TOut>(Func<TSuccess, RopValue<TOut, TFailure> f); 

public class Success<TSuccess, TFailure> : RopValue<TSuccess, TFailure> { 
    public override RopValue<TOut, TFailure> Bind<TOut>(Func<TSuccess, RopValue<TOut, TFailure> f) { 
     return f(this.Value); 
    } 
} 

public class Failure<TSuccess, TFailure> : RopValue<TSuccess, TFailure> { 
    public override RopValue<TOut, TFailure> Bind<TOut>(Func<TSuccess, RopValue<TOut, TFailure> f) { 
     return new Failure<TOut, TFailure>(this.Value); 
    } 
} 

Sau đó bạn có thể tránh tạo ra một giá trị giả vào lúc bắt đầu của chuỗi:

private static RopValue<Person, string> Validate(Person person) { 
    return CheckName(person) 
    .Bind(CheckEmail) 
    .Bind(CheckAge); 
} 
+0

Đã có cơ hội thích hợp để xem xét điều này ngay bây giờ. Khi bạn nói hàm Bind của tôi có kiểu sai, theo như tôi thấy, sự khác biệt thực sự duy nhất là bạn đã sửa lỗi đơn giản tiềm thức của tôi về giả sử kiểu đầu vào và đầu ra sẽ giống nhau. Bạn không chắc chắn tại sao điều đó sẽ thay đổi cách bạn có thể sử dụng nó trong trường hợp của tôi, vì tôi đang làm việc với một lớp Person tất cả các cách thức thông qua. Bạn có thể giải thích thêm một chút không? –

+0

Ngoài ra, tôi đã cố gắng di chuyển hàm Bind đến lớp cơ sở, điều này rất tốt cho thành công, vì tôi vừa trả về switchFunction (Value), nhưng tôi không thể tìm ra cái gì trong lớp Failure. Suy nghĩ của tôi là trả về "điều này" (là đối tượng Thất bại chính nó, tất nhiên là một đối tượng RopValue), nhưng điều này đã cho một lỗi trình biên dịch mà nó không thể chuyển đổi một Thất bại thành RopValue. Đúc cũng không giúp được gì. Tôi đã bỏ lỡ điều gì ở đây? Cảm ơn một lần nữa –

+0

@AvrohomYisroel - Xem cập nhật. – Lee

1

Lee đúng là hàm ràng buộc của bạn được xác định không chính xác.

Bind nên luôn luôn có một chữ ký kiểu đó trông giống như: m<'a> -> ('a -> m<'b>) -> m<'b>

tôi định nghĩa nó như thế này nhưng Lee là chức năng giống hệt nhau:

public static RopValue<TSuccess2, TFailure> Bind<TSuccess, TSuccess2, TFailure>(this RopValue<TSuccess, TFailure> input, Func<TSuccess, RopValue<TSuccess2, TFailure>> switchFunction) 
{ 
    if (input.IsSuccess) 
    { 
     return switchFunction(((Success<TSuccess,TFailure>)input).Value); 
    } 
    return new Failure<TSuccess2, TFailure>(((Failure<TSuccess, TFailure>)input).Value); 
} 

Kleisli Thành phần (>=>) có kiểu chữ ký giống như: ('a -> m<'b>) -> ('b -> m<'c>) -> 'a -> m<'c>

Bạn có thể xác định rằng sử dụng liên kết:

public static Func<TSuccess, RopValue<TSuccess2, TFailure>> Kleisli<TSuccess, TSuccess2, TFailure>(this Func<TSuccess, RopValue<TSuccess, TFailure>> switch1, Func<TSuccess, RopValue<TSuccess2, TFailure>> switch2) 
{ 
    return (inp => switch1(inp).Bind(switch2)); 
} 

Bạn có thể xác định phương pháp khuyến nông trên Func nhưng thủ đoạn này là nhận được biên dịch để thấy rằng những phương pháp khuyến nông có sẵn, một cái gì đó như thế này sẽ làm việc:

Func<Entry, RopValue<Request, string>> checkEmail = CheckEmail; 
var combined = checkEmail.Kleisli(CheckAge); 
RopValue<Request, string> result = combined(request); 

đâu request là dữ liệu của bạn để xác nhận.

Lưu ý rằng bằng cách tạo biến số Func, nó cho phép chúng tôi sử dụng phương pháp mở rộng.

+0

Câu trả lời của tôi cho phép thay đổi kiểu thành công, đó là thông số 'TOut'. – Lee

+0

@Lee Bạn nói đúng, tôi xin lỗi, tôi rõ ràng đang phát điên. – TheInnerLight

Các vấn đề liên quan