قالب وردپرس درنا توس
Home / IOS Development / Making illegal states urepresentable – Ole Begemann

Making illegal states urepresentable – Ole Begemann



I've said before that one of the most important benefits of a strong type of system is automatic and compiler-handed documentation.

An API with carefully selected input and output types is easier to use because the types establish a kind of dividend : Int Int [19659000] 19659004] Consider the following Swift function drawing as a simple example:

  func    /      divisor :    Int )     ] ->    Int 

Without knowing anything about the implementation of the function, you can deduce that it must perform integer division because the return type is unable to express fractional values. Contrary to the fact that the return type of the function was NSNummer that can express both integer and floating point values, you must trust that the behavior is adequately documented.

This technique for using types of documenting behaviors is becoming more and more useful as a type system's expressiveness grows. If Swift had a NonZeroInt type to express the term "any integer except null", the sharing function may be declared as follows:

  func    /    ( ]   ]:    Int     divisor :    NonZeroInt )    ->    Int 

Because the Type Controller Does not Allow You To Pass 0 as a divisor, you do not have to ask questions how the function handles a division with zero errors. Is it falling? Does it return a trash value? This is something that the first variant of the feature must document separately.

We can make this insight into a general rule: Use types to make illegal states unrepresentable in the program.

To learn more about doing this, check out Brandon Williams and Stephen Celi's new Point-Free video series. They talk a lot about this and related topics. The first eight episodes have been good and I strongly recommend the subscription. You learn a lot.

In episode 4 on algebraic data types, Brandon and Stephen discuss how enums and structs (or tuples) can be combined to design types that can accurately represent the desired states, but no more (all invalid do not say representative). Towards the end of the episode, Apple's URLSession API mentions a negative example of an API that does not use types as well as it should, which brings me to the subtitles of this article.

Swifts type The system is much more expressive than Objective-C. However, many of Apple's APIs still do not take full advantage of it, there is a lack of resources to update old APIs or to maintain Objective-C compatibility.

Rate the used method to make a network request on iOS:

  class    URLSession    {
      FUNC    dataTask  (  with    url  :    URL  
                     [19659000]]    @escaping    (???   data     URLResponse     Error )    ->    void ) 
          ->    URLSessionDataTask 
} 

The completion trader receives three optional values: Data? URLResponse? and Error? ]. It makes 2 × 2 × 2 = 8 possible states but how many of them are legal?

To quote Brandon and Stephen, there are many representative states here that do not make sense. Some are obviously invisible, and we can probably trust Apple's code to never call the finisher with all values ​​like zero or all are not null .

Error and error can not be nil at the same time

Other states are harder, and Brandon and Stephen made a small mistake: they assumed that the API would either return (a) one valid Data [19659069] and URLResponse or (b) an Error . After all, it should not be possible to get a non- null response and an error at the same time. Reasonable, right?

It turns out that this is incorrect. A URLResponse includes the server's HTTP response headers, and the API URLSession will always give you this value once it has received a valid reply header, even if the request fails at a later date (e.g. eg due to cancellation or timeout). Thus, it is expected that the completion manager will have a popular URLResponse and non- null error value (but no Data ).

If you are familiar with the URLSession delegation-based API, this may not be surprising to you because there are separate delegation methods for didReceiveResponse and didReceiveData ]. And to be fair, the documentation for dataTask (with: completionHandler :) also includes this case:

If a response from the server is received, regardless of whether the request completes succeeds or fails, ] the response parameter contains that information.

Still, I bet that this is a very popular misconception among cocoa developers. Just in the last four weeks, I saw two blog posts whose authors did the same mistake (or at least did not recognize the subtlety).

I absolutely love the irony in this: the fact that Brandon and Stephen, while pointing out an API due to poorly selected types, made an honest mistake like could have been prevented if the original API had used better types illustrates the point they made beautiful: a more strictly written API can prevent accidental abuse.

Example Code

If you want to check out URLSession its behavior itself, paste the following code into a Swift playground:

  import    Foundation 
 ]    PlaygroundSupport 

  ] // If this 404s, replace with a URL to another large file 
  la    bigFile    =    URL  (  strict ]:    "https://speed.hetzner.de/1GB.bin" )  

  la    task [19659005] =    URLSession    shared    dataTask  ( [19659000]] :    bigFile ).    {   (  data     response     error )    ] 
      press  (  "data:"  [19659105] data    as    Any ) 
      press  (  "reply: "    response    as    Any ) [19659116] press  (  "error:"     error    as    Any ). 
} 
  task    CV  () 

  // Cancel download after a few seconds 
  DispatchQueue .   main .      :   ].   now  ()    +    3 )    {
      task .   interrupt  () 
} [19659166] PlaygroundPage . [19659044] current .   needsIndefiniteExecution    =    true 

The code begins downloading a large file and then interrupting the request after a few seconds. As a result, the finisher is called with a non- null answer and error.

(This assumes that the specified time period is long enough to receive the replies from the server and too short to download to complete. If you are on a very slow or incredibly fast network, you may need to adjust the time parameter.) [19659158] What is the correct type?

Brandon and Stephen published their own follow-up of the issue as part of episode 9 of Point-Free. Their conclusion is that the "correct" parameter for the completion manager is:

  (  URLResponse  ?,    Result  <  Data     Error [19659093]> [19659004]) 

I disagree because I get valid data, but there seems to be no response. I think it should be:

  Results  < (  Data     URLResponse ),    (  Error     URL: Answer ?) > 

Translation: You will either get data and a response (as guaranteed is not null ), or an error and an optional response . Although my proposal is in conflict with the usual definition of the Result type, which limits the error parameter of the Error protocol - (Error, URLResponse?) Ten matches with Error . URLSession The API is particularly difficult due to the inadvertent behavior of the URLResponse parameter, but virtually all Apple's recall-based asynchronous APIs have the same anti-pattern that the specified types make illegal states representative.

How can we fix this?

The common approach in Swift is to define a Result type - a one that can represent either a general success value or an error. Recently, there has been another push (not the first) to add Result to the default library. If Results make it Swift 5, if Apple could even bigger if could import cocoa APIs from the form completionHandler: (A?, Error?) -> Feid as (Result ) -> Feid four representable states in two. Until then (if ever), I encourage you to do the conversion yourself.

On a longer schedule, Swift will get the correct language support for working with asynchronous APIs one day. It is likely that the solution that the community and the Swift team come up with will allow existing cocoa APIs to be ported to the new system, similar to how NSError ** the Objectives-C parameters have already been imported into Swift axis throwing features. Do not count on seeing this before Swift 6 early, though.


Source link