deterministicidempotent

What is the difference between an Idempotent and a Deterministic function?


Are idempotent and deterministic functions both just functions that return the same result given the same inputs?

Or is there a distinction that I'm missing? (And if there is a distinction, could you please help me understand what it is)


Solution

  • In more simple terms:

    Idempotency does not imply determinacy (as a function can alter state on the first call while being idempotent on subsequent calls), but all pure deterministic functions are inherently idempotent (as there is no internal state to persist between calls). Impure deterministic functions are not necessarily idempotent.

    Pure deterministic Impure deterministic Pure Nondeterministic Impure Nondeterministic Idempotent
    Input Only parameter arguments (incl. this) Only parameter arguments (incl. this) Parameter arguments and hidden state Parameter arguments and hidden state Any
    Output Only return value Return value or side-effects Only return value Return value or side-effects Any
    Side-effects None Yes None Yes After 1st call: Maybe.
    After 2nd call: None
    SQL Example UCASE CREATE TABLE GETDATE DROP TABLE
    C# Example String.IndexOf DateTime.Now Directory.Create(String)Footnote1

    A temporary note on non-scalar parameters, this, and mutating input arguments:

    (I'm currently unsure how instance methods in OOP languages (with their hidden this parameter) can be categorized as pure/impure or deterministic or not - especially when it comes to mutating the the target of this - so I've asked the experts in CS.SE to help me come to an answer - once I've got a satisfactory answer there I'll update this answer).

    A note on Exceptions

    Many (most?) programming languages today treat thrown exceptions as either a separate "kind" of return (i.e. "return to nearest catch") or as an explicit side-effect (often due to how that language's runtime works). However, as far as this answer is concerned, a given function's ability to throw an exception does not alter its pure/impure/deterministic/non-deterministic label - ditto idempotency (in fact: throwing is often how idempotency is implemented in the first place e.g. a function can avoid causing any side-effects simply by throwing right-before it makes those state changes - but alternatively it could simply return too.).

    So, for our CS-theoretical purposes, if a given function can throw an exception then you can consider the exception as simply part of that function's output. What does matter is if the exception is thrown deterministically or not, and if (e.g. List<T>.get(int index) deterministically throws if index < 0).

    Note that things are very different for functions that catch exceptions, however.

    Determinacy of Pure Functions

    For example, in SQL UCASE(val), or in C#/.NET String.IndexOf are both deterministic because the output depends only on the input. Note that in instance methods (such as IndexOf) the instance object (i.e. the hidden this parameter) counts as input, even though it's "hidden":

    "foo".IndexOf("o") == 1 // first cal
    "foo".IndexOf("o") == 1 // second call
    // the third call will also be == 1
    

    Whereas in SQL NOW() or in C#/.NET DateTime.UtcNow is not deterministic because the output changes even though the input remains the same (note that property getters in .NET are equivalent to a method that accepts no parameters besides the implicit this parameter):

     DateTime.UtcNow == 2016-10-27 18:10:01 // first call
     DateTime.UtcNow == 2016-10-27 18:10:02 // second call
    

    Idempotency

    A good example in .NET is the Dispose() method: See Should IDisposable.Dispose() implementations be idempotent?

    a Dispose method should be callable multiple times without throwing an exception.

    So if a parent component X makes an initial call to foo.Dispose() then it will invoke the disposal operation and X can now consider foo to be disposed. Execution/control then passes to another component Y which also then tries to dispose of foo, after Y calls foo.Dispose() it too can expect foo to be disposed (which it is), even though X already disposed it. This means Y does not need to check to see if foo is already disposed, saving the developer time - and also eliminating bugs where calling Dispose a second time might throw an exception, for example.

    Another (general) example is in REST: the RFC for HTTP1.1 states that GET, HEAD, PUT, and DELETE are idempotent, but POST is not ( https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html )

    Methods can also have the property of "idempotence" in that (aside from error or expiration issues) the side-effects of N > 0 identical requests is the same as for a single request. The methods GET, HEAD, PUT and DELETE share this property. Also, the methods OPTIONS and TRACE SHOULD NOT have side effects, and so are inherently idempotent.

    So if you use DELETE then:

    Client->Server: DELETE /foo/bar
    // `foo/bar` is now deleted
    Server->Client: 200 OK
    Client->Server DELETE /foo/bar
    // foo/bar` is already deleted, so there's nothing to do, but inform the client that foo/bar doesn't exist
    Server->Client: 404 Not Found
    // the client asks again:
    Client->Server: DELETE /foo/bar
    // foo/bar` is already deleted, so there's nothing to do, but inform the client that foo/bar doesn't exist
    Server->Client: 404 Not Found
    

    So you see in the above example that DELETE is idempotent in that the state of the server did not change between the last two DELETE requests, but it is not deterministic because the server returned 200 for the first request but 404 for the second request.