DMN 1.2 RTF Avatar
  1. OMG Issue

DMN12 — Expected behavior for list/sort built-in functions in combination with singleton lists

  • Key: DMN12-210
  • Status: closed  
  • Source: ACTICO ( Daniel Thanner)
  • Summary:

    The DMN specification says about singleton lists:

    • Chapter 10.3.1.4 on page 110: "A singleton list is equal to its single item, i.e., [e] = e for all FEEL expressions e."
    • Chapter 10.3.2.5 on page 113: “Therefore, any function or operator that expects a list as input but instead receives a non-list semantic domain element e behaves as if it had received [e] as input.”

    This works for most expressions and built-in functions, but for list built-in functions and the sort built-in function the behavior is ambiguous. This leads to different possible valid results for a FEEL expression. The problem here is that the further processing of this result (in DMN and/or other FEEL expressions) leads to different behavior/results of subsequent (boxed) expressions.

    Examples of ambiguous FEEL expressions:

    • distinct values(["a", ["a"]])
      • possible results: ["a"] or [["a"]] or ["a", ["a"]]
    • union(["a"], [["a"]])
      • possible results: ["a"] or ["a", ["a"]]
    • index of(["a", ["a"]], "a")
      • possible results: [1,2] or [1]
    • count([[]])
      • possible results: 0 or 1
    • list contains(["a", ["b"]], "b")
      • possible results: true or false
    • min([1],2)
      • possible results: [1] or 1
    • flatten([“a”, [], [[]]])
      • possible results: [“a”] or [“a”, []]

    Therefore we think, the specification must be detailed to ensure cross vendor interoperability.

    Our proposal for DMN 1.2 is not a change of the existing behavior, but a more detailed description of the expected behavior:

    Add the following 5 bullet points to chapter 10.3.4.4 List functions before table 61:

    • List built-in functions work on the passed lists as they are. A single element may be converted to a singleton list where the parameter domain requires this. (For example count(), list con-tains(), index of()).
    • If a built-in function returns a list, any list item originating from the parameters are pre-served, i.e. singleton lists remain (for example reverse(), sort(), min()).
    • To operate on list items, singleton lists may be resolved to their single item (for example min(), and(), mean()).
    • If a list built-in function identifies list items, FEEL equality is used (for example index of(), list contains()).
    • If FEEL equality matches two or more list elements and the built-in function has to decide which equal element should be returned, the function MUST return the element with the lowest position. (For example distinct values())

    Additionally the description of the flatten() built-in function should be changed to:

    “flatten nested lists and removes empty and nested empty lists”

    Additionally it may be helpful to categorize the list built-in functions to the following categories:

    • Functions searching for list items:
      list contains(), index of()
    • Functions creating a new list with additional or removed items:
      sublist(), append(), concatenate(), insert before(), remove()
    • Functions changing the list order of items:
      reverse(), sort()
    • Functions operating on list items and calculating a result:
      count(), min(), max(), sum(), mean(), and(), or()
    • Set theory functions:
      distinct values(), flatten(), union()


    We could add a new column “category” to table 61 or divide table 61 into more tables.

    Examples for the above FEEL expressions with the new applied specification changes:

    • distinct values(["a", ["a"]]) = [“a”]
    • union(["a"], [["a"]]) = [“a”]
    • index of(["a", ["a"]], "a") = [1, 2]
    • count([[]]) = 1
    • list contains(["a", ["b"]], "b") = true
    • min([1],2) = [1]
    • flatten([“a”, [], [[]]]) = [“a”]
    Even more examples

    list contains([], []) = false // processes passed list as is. This list is empty and cannot contain items.
    list contains([[]], []) = true // note: different result as for []
    list contains("a", "a") = true // automatic conversion from first parameter "a" to ["a"]
    list contains(["a"], "a") = true
    list contains([["a"]], "a") = true // FEEL equality is applied where [e] = e
    list contains(["a", "b", []], []) = true
    list contains(["a", "b", [[]]], []) = true

    index of([], []) = [] // processes passed list as is. This list is empty and cannot contain items.
    index of([[]], []) = [1] // note: different result as for []
    index of("a", "a") = [1] // automatic conversion from first parameter "a" to ["a"]
    index of(["a"], "a") = [1]
    index of([["a"]], "a") = [1] // FEEL equality is applied where [e] = e
    index of(["a", "b", []], []) = [3]
    index of (["a", "b", [[]]], []) = [3]

    sublist([],1,1) = null // invalid position
    sublist([[]],1,1) = [[]]
    sublist("a", 1, 1) = ["a"] // automatic conversion from first parameter "a" to ["a"]
    sublist(["a"], 1, 1) = ["a"]
    sublist([["a"]], 1, 1) = [["a"]]
    sublist(["a", "b", []], 3, 1) = [[]]

    append([], 1) = [1]
    append([[]], 1) = [[], 1]
    append("a", 1) = ["a", 1] // automatic conversion from first parameter "a" to ["a"]
    append(["a"], 1) = ["a", 1]
    append([["a"]], 1) = [["a"], 1]

    concatenate([], []) = []
    concatenate([[]], []) = [[]]
    concatenate("a", []) = ["a"] // automatic conversion from first parameter "a" to ["a"]
    concatenate(["a"], []) = ["a"]
    concatenate([["a"]], []) = [["a"]]

    insert before([], 1, "a") = null // invalid position
    insert before([[]], 1, "a") = ["a", []]
    insert before("a", 1, "b") = ["b", "a"] // automatic conversion from first parameter "a" to ["a"]
    insert before(["a"], 1, "b") = ["b", "a"]
    insert before([["a"]], 1, "b") = ["b", ["a"]]

    remove([], 1) = null // invalid position
    remove([[]], 1) = []
    remove("a", 1) = [] // automatic conversion from first parameter "a" to ["a"]
    remove(["a"], 1) = []
    remove([["a"]], 1) = []

    sort([[[]], [], ["a"], [["a"]]], function (x,y) count(x) > count(y) ) = [[[]], ["a"], [["a"]], []]

    reverse([]) = []
    reverse([[]]) = [[]]
    reverse("a") = ["a"] // automatic conversion from first parameter "a" to ["a"]
    reverse(["a"]) = ["a"]
    reverse([["a"]]) = [["a"]]

    min([1], [2]) = [1] // return list item
    max([1], [2]) = [2] // return list item

    // mean(), sum(), and(), or() do arithmetic operations and therefore do never return a list

    count([]) = 0
    count([[]]) = 1 // note: different result as for []
    count("a") = 1 // automatic conversion from first parameter "a" to ["a"]
    count(["a"]) = 1
    count([["a"]]) = 1

    distinct values([[[]], [], "a", ["a"], [["a"]]]) = [[[]], "a"] // uses FEEL equality, preserves items, chooses for equal list items item with lower list position

    flatten([[[]], [], "a", ["a"], [["a"]]]) = ["a", "a", "a"] // removes empty lists

    union([[], [[]], "a", ["a"], [["a"]]], [[["a"]], ["a"], "a", [[]], []]) = [[], "a"]
    // uses distinct values(concatenate())

  • Reported: DMN 1.1 — Thu, 19 Oct 2017 14:27 GMT
  • Disposition: Resolved — DMN 1.2
  • Disposition Summary:

    Replace e=[e] with implicit de-listing to avoid argument domain error

    See attached word doc

  • Updated: Wed, 3 Oct 2018 14:17 GMT
  • Attachments: