Có bắt / ném ngoại lệ làm cho một phương pháp thuần túy không tinh khiết?


27

Các ví dụ mã sau đây cung cấp ngữ cảnh cho câu hỏi của tôi.

Lớp phòng được khởi tạo với một đại biểu. Trong lần thực hiện đầu tiên của lớp Room, không có người bảo vệ chống lại các đại biểu ném ngoại lệ. Các ngoại lệ như vậy sẽ xuất hiện ở thuộc tính Bắc, nơi đại biểu được đánh giá (lưu ý: phương thức Main () biểu thị cách sử dụng một thể hiện của Phòng trong mã máy khách):

public sealed class Room
{
    private readonly Func<Room> north;

    public Room(Func<Room> north)
    {
        this.north = north;
    }

    public Room North
    {
        get
        {
            return this.north();
        }
    }

    public static void Main(string[] args)
    {
        Func<Room> evilDelegate = () => { throw new Exception(); };

        var kitchen = new Room(north: evilDelegate);

        var room = kitchen.North; //<----this will throw

    }
}

Vì tôi không muốn tạo ra đối tượng hơn là khi đọc thuộc tính North, tôi thay đổi hàm tạo thành riêng tư và giới thiệu một phương thức nhà máy tĩnh có tên là Tạo (). Phương thức này bắt ngoại lệ do đại biểu ném và ném ngoại lệ bao bọc, có một thông báo ngoại lệ có ý nghĩa:

public sealed class Room
{
    private readonly Func<Room> north;

    private Room(Func<Room> north)
    {
        this.north = north;
    }

    public Room North
    {
        get
        {
            return this.north();
        }
    }

    public static Room Create(Func<Room> north)
    {
        try
        {
            north?.Invoke();
        }
        catch (Exception e)
        {
            throw new Exception(
              message: "Initialized with an evil delegate!", innerException: e);
        }

        return new Room(north);
    }

    public static void Main(string[] args)
    {
        Func<Room> evilDelegate = () => { throw new Exception(); };

        var kitchen = Room.Create(north: evilDelegate); //<----this will throw

        var room = kitchen.North;
    }
}

Khối thử bắt có làm cho phương thức Tạo () không trong sạch không?


1
Lợi ích của chức năng Tạo phòng là gì; nếu bạn sử dụng một đại biểu tại sao lại gọi nó trước khi khách hàng gọi 'North'?
SH-

1
@SH Nếu đại biểu ném ngoại lệ, tôi muốn tìm hiểu khi tạo đối tượng Phòng. Tôi không muốn khách hàng tìm thấy ngoại lệ khi sử dụng, mà là khi tạo. Phương thức Tạo là nơi hoàn hảo để vạch trần các đại biểu xấu xa.
Rock Anthony Johnson

3
@RockAnthonyJohnson, Vâng. Bạn làm gì khi thực hiện ủy nhiệm chỉ có thể làm việc lần đầu tiên hoặc trả lại một phòng khác trong cuộc gọi thứ hai? Gọi đại biểu có thể được xem xét / gây ra tác dụng phụ?
SH-

4
Hàm gọi hàm không tinh khiết là hàm không tinh khiết, vì vậy nếu đại biểu không trong sạch Createcũng không tinh khiết, vì nó gọi nó là hàm.
Idan Arye

2
CreateChức năng của bạn không bảo vệ bạn khỏi bị ngoại lệ khi nhận tài sản. Nếu đại biểu của bạn ném, trong cuộc sống thực, rất có khả năng nó sẽ chỉ ném trong một số điều kiện. Rất có thể là các điều kiện để ném không có mặt trong quá trình xây dựng, nhưng chúng có mặt khi nhận được tài sản.
Bart van Ingen Schenau

Câu trả lời:


26

Vâng. Đó thực sự là một chức năng không tinh khiết. Nó tạo ra một hiệu ứng phụ: việc thực hiện chương trình tiếp tục ở một nơi khác ngoài nơi mà hàm dự kiến ​​sẽ trả về.

Để biến nó thành một hàm thuần túy, returnmột đối tượng thực tế đóng gói giá trị mong đợi từ hàm và một giá trị chỉ ra một điều kiện lỗi có thể xảy ra, như một Maybeđối tượng hoặc một đối tượng Đơn vị công việc.


16
Hãy nhớ rằng, "tinh khiết" chỉ là một định nghĩa. Nếu bạn cần một hàm ném, nhưng theo mọi cách khác thì nó trong suốt về mặt tham chiếu, thì đó là những gì bạn viết.
Robert Harvey

1
Trong haskell ít nhất bất kỳ chức năng nào cũng có thể ném nhưng bạn phải ở trong IO để bắt, một chức năng thuần túy đôi khi ném thường được gọi là một phần
jk.

4
Tôi đã có một epiphany vì nhận xét của bạn. Trong khi tôi bị hấp dẫn về mặt trí tuệ bởi sự thuần khiết, điều tôi thực sự cần trong thực tiễn là tính quyết đoán và tính minh bạch tôn kính. Nếu tôi đạt được độ tinh khiết, đó chỉ là một phần thưởng bổ sung. Cảm ơn bạn. FYI, điều khiến tôi đi theo con đường này là thực tế là Microsoft IntelliTest chỉ có hiệu quả đối với mã mang tính quyết định.
Rock Anthony Johnson

4
@RockAnthonyJohnson: Chà, tất cả các mã nên có tính xác định hoặc ít nhất là xác định càng tốt. Minh bạch tham chiếu chỉ là một cách để có được tính quyết định đó. Một số kỹ thuật, như các luồng, có xu hướng hoạt động chống lại tính xác định đó, do đó, có các kỹ thuật khác như Nhiệm vụ cải thiện xu hướng không xác định của mô hình luồng. Những thứ như bộ tạo số ngẫu nhiên và bộ mô phỏng Monte-Carlo cũng mang tính quyết định, nhưng theo một cách khác dựa trên xác suất.
Robert Harvey

3
@zzzzBov: Tôi không xem xét định nghĩa thực tế, nghiêm ngặt về "thuần túy" tất cả những gì quan trọng. Xem bình luận thứ hai, ở trên.
Robert Harvey

12

Vâng, vâng ... và không.

Một hàm thuần túy phải có Độ trong suốt tham chiếu - nghĩa là, bạn sẽ có thể thay thế bất kỳ cuộc gọi nào có thể thành một hàm thuần túy bằng giá trị được trả về, mà không thay đổi hành vi của chương trình. * Hàm của bạn được đảm bảo luôn luôn ném cho một số đối số nhất định, do đó không có giá trị trả về để thay thế lệnh gọi hàm, vì vậy thay vào đó hãy bỏ qua nó. Hãy xem xét mã này:

{
    var ignoreThis = func(arg);
}

Nếu funclà thuần túy, một trình tối ưu hóa có thể quyết định func(arg)có thể được thay thế bằng kết quả của nó. Vẫn chưa biết kết quả là gì, nhưng có thể nói rằng nó không được sử dụng - vì vậy nó chỉ có thể suy ra tuyên bố này không có tác dụng và loại bỏ nó.

Nhưng nếu func(arg)tình cờ ném, tuyên bố này sẽ làm một cái gì đó - nó ném một ngoại lệ! Vì vậy, trình tối ưu hóa không thể loại bỏ nó - không thành vấn đề nếu hàm được gọi hay không.

Nhưng...

Trong thực tế, vấn đề này rất ít. Một ngoại lệ - ít nhất là trong C # - là một cái gì đó đặc biệt. Bạn không nên sử dụng nó như một phần của luồng kiểm soát thông thường của mình - bạn phải thử và bắt nó, và nếu bạn bắt được một cái gì đó xử lý lỗi để hoàn nguyên những gì bạn đang làm hoặc bằng cách nào đó vẫn hoàn thành nó. Nếu chương trình của bạn không hoạt động chính xác vì mã bị lỗi đã được tối ưu hóa, bạn đang sử dụng ngoại lệ sai (trừ khi đó là mã kiểm tra và khi bạn xây dựng để kiểm tra ngoại lệ thì không nên tối ưu hóa).

Điều đó đang được nói ...

Đừng ném ngoại lệ khỏi các hàm thuần túy với ý định rằng chúng sẽ bị bắt - có một lý do chính đáng để các ngôn ngữ chức năng thích sử dụng các đơn vị thay vì ngoại lệ ngăn xếp.

Nếu C # có một Errorlớp như Java (và nhiều ngôn ngữ khác), tôi đã đề nghị ném một Errorthay vì một Exception. Nó chỉ ra rằng người sử dụng hàm đã làm gì đó sai (đã truyền một hàm ném) và những thứ như vậy được cho phép trong các hàm thuần túy. Nhưng C # không có Errorlớp và các ngoại lệ lỗi sử dụng dường như xuất phát từ đó Exception. Đề nghị của tôi là ném một cái ArgumentException, làm rõ rằng hàm được gọi với một đối số xấu.


* Tính toán nói. Hàm Fibonacci được triển khai bằng cách sử dụng đệ quy ngây thơ sẽ mất nhiều thời gian cho số lượng lớn và có thể làm cạn kiệt tài nguyên của máy, nhưng những thứ này vì với thời gian và bộ nhớ vô hạn, hàm sẽ luôn trả về cùng giá trị và sẽ không có tác dụng phụ (ngoài việc phân bổ bộ nhớ và thay đổi bộ nhớ đó) - nó vẫn được coi là thuần túy.


1
+1 Đối với việc sử dụng ArgumentException được đề xuất của bạn và để đề xuất của bạn không "ném ngoại lệ khỏi các hàm thuần túy với ý định rằng chúng sẽ bị bắt".
Rock Anthony Johnson

Đối số đầu tiên của bạn (cho đến khi "Nhưng ...") dường như không có cơ sở đối với tôi. Ngoại lệ là một phần của hợp đồng của chức năng. Về mặt khái niệm, bạn có thể viết lại bất kỳ chức năng nào (value, exception) = func(arg)và loại bỏ khả năng ném ngoại lệ (trong một số ngôn ngữ và đối với một số loại ngoại lệ bạn thực sự cần liệt kê nó với định nghĩa, giống như đối số hoặc giá trị trả về). Bây giờ nó sẽ hoàn toàn thuần khiết như trước đây (với điều kiện nó luôn trả về aka ném một ngoại lệ cho cùng một đối số). Ném một ngoại lệ không phải là một tác dụng phụ, nếu bạn sử dụng cách giải thích này.
AnoE

@AnoE Nếu bạn đã viết functheo cách đó, khối tôi đã đưa ra có thể đã được tối ưu hóa đi, bởi vì thay vì mở khóa ngăn xếp, ngoại lệ ném sẽ được mã hóa ignoreThis, mà chúng ta bỏ qua. Không giống như các ngoại lệ làm thư giãn ngăn xếp, ngoại lệ được mã hóa theo kiểu trả về không vi phạm độ tinh khiết - và đó là lý do tại sao nhiều ngôn ngữ chức năng thích sử dụng chúng.
Idan Arye

Chính xác ... bạn sẽ không bỏ qua ngoại lệ cho chuyển đổi tưởng tượng này. Tôi đoán tất cả chỉ là một vấn đề về ngữ nghĩa / định nghĩa, tôi chỉ muốn chỉ ra rằng sự tồn tại hoặc xuất hiện của các ngoại lệ không nhất thiết làm mất đi tất cả các khái niệm về độ tinh khiết.
AnoE

1
@AnoE Nhưng mã tôi đã viết sẽ bỏ qua ngoại lệ cho phép chuyển đổi tưởng tượng này. func(arg)sẽ trả lại tuple (<junk>, TheException()), và do đó ignoreThissẽ chứa tuple (<junk>, TheException()), nhưng vì chúng ta không bao giờ sử dụng ignoreThisngoại lệ sẽ không có tác dụng gì.
Idan Arye

1

Một xem xét là khối thử - bắt không phải là vấn đề. (Dựa trên ý kiến ​​của tôi cho câu hỏi trên).

Vấn đề chính là bất động sản Bắc là một cuộc gọi I / O .

Tại thời điểm thực thi mã, chương trình cần kiểm tra I / O được cung cấp bởi mã máy khách. (Sẽ không có liên quan rằng đầu vào ở dạng đại biểu hoặc đầu vào, trên danh nghĩa, đã được thông qua).

Khi bạn mất quyền kiểm soát đầu vào, bạn không thể đảm bảo chức năng là thuần túy. (Đặc biệt nếu chức năng có thể ném).


Tôi không rõ lý do tại sao bạn không muốn kiểm tra cuộc gọi để di chuyển [Kiểm tra] phòng? Theo nhận xét của tôi cho câu hỏi:

Vâng. Bạn làm gì khi thực hiện ủy nhiệm chỉ có thể làm việc lần đầu tiên hoặc trả lại một phòng khác trong cuộc gọi thứ hai? Gọi đại biểu có thể được xem xét / gây ra tác dụng phụ?

Như Bart van Ingen Schenau đã nói ở trên,

Chức năng Tạo của bạn không bảo vệ bạn khỏi bị ngoại lệ khi nhận tài sản. Nếu đại biểu của bạn ném, trong cuộc sống thực, rất có khả năng nó sẽ chỉ ném trong một số điều kiện. Rất có thể là các điều kiện để ném không có mặt trong quá trình xây dựng, nhưng chúng có mặt khi nhận được tài sản.

Nói chung, bất kỳ loại tải lười biếng nào đều trì hoãn các lỗi cho đến thời điểm đó.


Tôi sẽ đề nghị sử dụng phương pháp Di chuyển [Kiểm tra]. Điều này sẽ cho phép bạn tách các khía cạnh I / O không tinh khiết thành một nơi.

Tương tự như câu trả lời của Robert Harvey :

Để biến nó thành một hàm thuần túy, trả về một đối tượng thực tế đóng gói giá trị mong đợi từ hàm và một giá trị chỉ ra một điều kiện lỗi có thể xảy ra, như đối tượng Có thể hoặc đối tượng Đơn vị công việc.

Nó sẽ tùy thuộc vào người viết mã để xác định cách xử lý ngoại lệ (có thể) từ đầu vào. Sau đó, phương thức có thể trả về một đối tượng Room hoặc đối tượng Null Room hoặc có thể loại bỏ ngoại lệ.

Điểm này phụ thuộc vào:

  • Tên miền phòng có coi Ngoại lệ phòng là Null hay gì đó tệ hơn không.
  • Làm thế nào để thông báo mã khách hàng gọi miền Bắc trên phòng Null / Exception. (Bail / Biến trạng thái / Trạng thái toàn cầu / Trả lại một đơn vị / Bất cứ điều gì; Một số thuần túy hơn những người khác :)).

-1

Hmm ... tôi không cảm thấy đúng về điều này. Tôi nghĩ những gì bạn đang cố gắng làm là đảm bảo người khác làm đúng công việc của họ, mà không biết họ đang làm gì.

Vì chức năng được chuyển qua bởi người tiêu dùng, có thể được viết bởi bạn hoặc bởi người khác.

Điều gì xảy ra nếu hàm truyền vào không hợp lệ để chạy tại thời điểm đối tượng Room được tạo?

Từ mã, tôi không thể chắc chắn những gì miền Bắc đang làm.

Giả sử, nếu chức năng là đặt phòng, và nó cần thời gian và thời gian đặt phòng, thì bạn sẽ có ngoại lệ nếu bạn chưa có thông tin sẵn sàng.

Nếu nó là một chức năng chạy dài thì sao? Bạn sẽ không muốn chặn chương trình của mình trong khi tạo đối tượng Room, nếu bạn cần tạo 1000 đối tượng Room thì sao?

Nếu bạn là người sẽ viết người tiêu dùng tiêu thụ lớp này, bạn sẽ đảm bảo chức năng được truyền vào được viết chính xác, phải không?

Nếu có một người khác viết thư cho người tiêu dùng, anh ấy / cô ấy có thể không biết điều kiện "đặc biệt" của việc tạo đối tượng Room, điều này có thể gây ra sự hành quyết và họ sẽ gãi đầu cố gắng tìm hiểu tại sao.

Một điều nữa là, không phải lúc nào cũng là một điều tốt để ném một ngoại lệ mới. Lý do là nó sẽ không chứa thông tin của ngoại lệ ban đầu. Bạn biết rằng bạn đã gặp lỗi (một mesaage thân thiện cho ngoại lệ chung), nhưng bạn sẽ không biết lỗi đó là gì (tham chiếu null, chỉ mục nằm ngoài giới hạn, lệch 0, v.v.). Thông tin này rất quan trọng khi chúng ta cần điều tra.

Bạn chỉ nên xử lý ngoại lệ khi bạn biết những gì và làm thế nào để xử lý nó.

Tôi nghĩ rằng mã gốc của bạn là đủ tốt, điều duy nhất là bạn có thể muốn xử lý ngoại lệ trên "Chính" (hoặc bất kỳ lớp nào biết cách xử lý nó).


1
Nhìn lại ví dụ mã thứ 2; Tôi khởi tạo ngoại lệ mới với ngoại lệ thấp hơn. Toàn bộ dấu vết ngăn xếp được bảo tồn.
Rock Anthony Johnson

Rất tiếc. Lỗi của tôi.
dùng251630

-1

Tôi sẽ không đồng ý với các câu trả lời khác và nói Không . Các ngoại lệ không làm cho một chức năng thuần túy khác trở nên không trong sạch.

Bất kỳ việc sử dụng ngoại lệ nào cũng có thể được viết lại để sử dụng kiểm tra rõ ràng cho kết quả lỗi. Trường hợp ngoại lệ chỉ có thể được coi là cú pháp đường trên đầu trang này. Đường cú pháp thuận tiện không làm cho mã tinh khiết không tinh khiết.


1
Thực tế là bạn có thể viết lại tạp chất đi không làm cho hàm thuần túy. Haskell là một bằng chứng cho thấy việc sử dụng các hàm không tinh khiết có thể được viết lại bằng các hàm thuần túy - nó sử dụng các đơn nguyên để mã hóa bất kỳ hành vi không tinh khiết nào trong các kiểu trả về các hàm thuần túy. Sự tồn tại của các đơn vị IO không ngụ ý rằng IO thông thường là thuần túy - tại sao sự tồn tại của các đơn vị ngoại lệ có nghĩa là các ngoại lệ thông thường là thuần túy?
Idan Arye

@IdanArye: Tôi cho rằng không có tạp chất ở nơi đầu tiên. Ví dụ viết lại chỉ để chứng minh điều đó. IO thông thường không tinh khiết vì nó gây ra các tác dụng phụ có thể quan sát được hoặc có thể trả về các giá trị khác nhau với cùng một đầu vào do một số đầu vào bên ngoài. Ném một ngoại lệ không làm bất cứ điều gì trong số những điều đó.
JacquesB

Mã miễn phí tác dụng phụ là không đủ. Độ trong suốt tham chiếu cũng là một yêu cầu cho độ tinh khiết của chức năng.
Idan Arye

1
Chủ nghĩa quyết định là không đủ cho tính minh bạch tham chiếu - tác dụng phụ cũng có thể mang tính quyết định. Độ trong suốt của tham chiếu có nghĩa là biểu thức có thể được thay thế bằng kết quả của nó - vì vậy, cho dù bạn đánh giá biểu thức minh bạch tham chiếu bao nhiêu lần, theo thứ tự nào và liệu bạn có đánh giá chúng hay không - kết quả sẽ giống nhau. Nếu một ngoại lệ được ném một cách dứt khoát, chúng ta sẽ tốt với tiêu chí "bất kể bao nhiêu lần", nhưng không phải với các tiêu chí khác. Tôi đã tranh luận về tiêu chí "có hay không" trong câu trả lời của riêng tôi, (tiếp ...)
Idan Arye

1
(... tiếp) và đối với "theo thứ tự nào" - nếu fgcả hai hàm thuần túy, thì f(x) + g(x)sẽ có cùng kết quả bất kể thứ tự bạn đánh giá f(x)g(x). Nhưng nếu f(x)ném Fg(x)ném G, thì ngoại lệ được ném từ biểu thức này sẽ là Fnếu f(x)được đánh giá trước và Gnếu g(x)được đánh giá trước - đây không phải là cách các hàm thuần túy nên hoạt động! Khi ngoại lệ được mã hóa theo kiểu trả về, kết quả đó sẽ kết thúc như một thứ giống như Error(F) + Error(G)độc lập với thứ tự đánh giá.
Idan Arye
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.