Can I put a closeable resource in a List, an Optional or my own generic type?

← Error Handling and Exceptions · Ref: Q1780

No. A closeable type (one with an 'operator close') is never a type argument of a generic type or a generic function. List of TCP, Optional of Res, Dict of (String, HTTP), a Box of Res of your own and Acceptor of TCP are all error E81061, and so are the same parameterisations when they are INFERRED - [server], Optional(res), {"key": res}, or a generic function called with a closeable, which infers T as that type:

  keepAll()
    -> server as TCP
    servers <- [server]   // ERROR E81061 - a List of TCP

A generic's 'T constrain by Res', where Res is closeable, is refused at its declaration too: it could only ever be given a closeable.

The reason is ownership. A closeable value has exactly one owner, and the compiler must see that owner close it: a try-with-resources resource, a property closed by its type's generated close, or the caller of a callable that returns a new one. A generic type or function has no such owner, so what it held would never be closed - or would be closed while somebody else still owned it.

A closeable can be a PARAMETER type: that is a loan, and the lender keeps ownership. So pick one of three shapes:

1. Hold the resources as properties of a type you own, which declares 'default operator close' - its generated close closes them.
2. Acquire each one as a try-with-resources resource where it is used.
3. Lend it to the code that needs it, as a parameter, instead of collecting it.

A callback that receives a resource is declared as a non-generic function type with that parameter - the way TCPReadyHandler takes a TCP - never as Acceptor of TCP.

The program below holds two resources as properties of an owning type, lends each one to a function, and keeps a third in a try; every close prints, so you can see each resource closed exactly once.

Example

defines module qa.errorhandling.closeablenevertypeargument

  defines class

    Line
      text as String?

      default private Line()

      Line()
        -> initialText as String
        text: initialText

      greeting() as pure
        <- rtn as String: `hello ${text}`

      operator close as pure
        stdout <- Stdout()
        stdout.println(`closed ${text}`)

      default operator ?

    <?- Owns its resources as properties - not a List of Line - and its generated close closes them, last first -?>
    Lines
      first as Line?
      second as Line?

      default private Lines()

      Lines()
        ->
          firstText as String
          secondText as String
        first: Line(firstText)
        second: Line(secondText)

      greetAll()
        show(first)
        show(second)

      default operator close

      default operator ?

  defines function

    <?- A closeable as a PARAMETER is a loan: the lender keeps ownership and closes it -?>
    show()
      -> line as Line
      stdout <- Stdout()
      stdout.println(line.greeting())

  defines program

    CloseableTypeArgumentDemo()

      try
        -> lines <- Lines("one", "two")
        lines.greetAll()

      try
        -> line <- Line("three")
        show(line)

Common mistakes

E81061 — A closeable type is never a type argument: [line] would be a List of Line, and a List has no owner that closes what it holds. Lend the resource as a parameter instead, or hold it as a property of a type you own. See ek9 -h E81061 for details.

Incorrect:

      try
        -> line <- Line("three")
        lines <- [line]
        show(line)

Correct:

      try
        -> line <- Line("three")
        show(line)
Other ways to ask this
  • Why is 'servers <- [TCP(properties)]' an error?
  • Can I keep several open connections in a List and close them later?
  • Why can't I write Optional of TCP or Dict of (String, HTTP)?
  • Can a generic type parameter be constrained by a closeable type?
  • What does E81061 mean?

Coming from another language?

Java: a List<Socket> or Optional<Connection> compiles, and closing every element is left to a finally loop that is easy to get wrong or forget. C#: a List<IDisposable> is the same; 'using' only covers a single variable. Rust: a Vec<File> drops each element when the Vec is dropped, because ownership moves into the container. EK9: a container never owns a resource; ownership stays with a try, a property of a type you write, or the caller of a factory, and the compiler refuses the type argument (E81061).

Keywords: List, try-with-resources, type argument, operator close, generic, ownership, Dict, E81061, constrain by, Optional, closeable, close, loan