Skip to content

Instantly share code, notes, and snippets.

@matfournier
Last active July 1, 2021 22:03
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save matfournier/65247e627b1a7cdf77e74d655b698d09 to your computer and use it in GitHub Desktop.
Save matfournier/65247e627b1a7cdf77e74d655b698d09 to your computer and use it in GitHub Desktop.
Nim interface example
import
# map,
# entity,
strformat
type
Terminal* = ref TerminalObj
TerminalObj* = object of RootObj
layer*: proc (i: int)
EchoTerminal* = ref EchoTerminalObj # version that echos to console
EchoTerminalObj* = object of TerminalObj
id*: string
layerValue*: int
BearTerminal* = ref BearTerminalObj # version that uses BearLibTerminal
BearTerminalObj* = object of TerminalObj
# constructor for console version
proc newEchoTerminal*(id: string): EchoTerminal =
var t = EchoTerminal()
t.id = id
# if you didn't implement t.layer it compiles but then doStuff explodes at runtime :(
t.layer = proc(i: int) =
t.layerValue = i
return t
# actually use it
proc doStuff(terminal: Terminal) =
terminal.layer(1)
when isMainModule:
let terminal = newEchoTerminal("id")
echo fmt"terminal id is {terminal.id}"
doStuff(terminal)
echo fmt"terminal layer is still {terminal.layerValue}"
terminal.layer(4)
echo fmt"terminal layer is now {terminal.layervalue}"
@deech
Copy link

deech commented Jul 1, 2021

Something like this? No runtime issues because we use var and not ref:

import strformat
type
  Terminal = object of RootObj
  EchoTerminal = object of Terminal
    id: string
    layerValue: int
  BearTerminal = object of Terminal

proc layer(t:var EchoTerminal,i:int) = t.layerValue = i
proc id(t:EchoTerminal):string = "Echo terminal"

proc doStuff(t: var EchoTerminal) =
  t.layer(1)

when isMainModule:
  var terminal = EchoTerminal(id: "id")
  echo fmt"terminal id is {terminal.id}"
  doStuff(terminal)
  echo fmt"terminal layer is still {terminal.layerValue}"
  terminal.layer(4)
  echo fmt"terminal layer is now {terminal.layervalue}"

@saem
Copy link

saem commented Jul 1, 2021

What @deech said, that's why I don't end up needing interfaces or any run time business.

Between avoiding ref types unless I really need the many to one and procs which are statically dispatched, it's pretty great.

@deech
Copy link

deech commented Jul 1, 2021

If you wanted proc doStuff(t:Terminal) what should it do when called with Terminal()? Nim doesn't have the concept of a set of functions that a type has to implement.

Edit: it has concepts but that's not stable yet and I haven't had good luck with them.

@saem
Copy link

saem commented Jul 1, 2021

Do your terminals have to open for extension outside this module? If not, why not use a variant and dispatch on that particular kind?

import strformat
type
  TerminalKind = enum
    tkEcho, tkBear
  Terminal = object
    layer: proc (i: int) # everything above the case is always present and needs to be part of construction
    case kind*: TerminalKind
    of tkEcho:
      id: string
      layerValue: int
    of tkBear: discard

# write your procs as if it's one thing, if you care about terminal type use a case

@matfournier
Copy link
Author

Yeah. This all stemmed from trying to figure out how to unit test proc's that return void. I have a ton of code doing stuff with BearLibTerminal and it all returns void --I have no way to test a ton of my codebase atm.

@matfournier
Copy link
Author

@seam:

e.g.

type
  TerminalKind = enum 
    tkEcho, tkbear
  ITerminal = object
    layer: proc (i: int)
    case kind*: TerminalKind
    of tkEcho:
      layerValue: int
    of tkBear:
      discard

proc newEchoIterminal: ITerminal =
  ITerminal(
    kind: tkEcho,
    layerValue: 0,
    layer: proc(i: int) =
      echo fmt"{i} was passed in"
      # can't access layervalue to set it here in this proc? 
    )

@saem
Copy link

saem commented Jul 1, 2021

Yup, that's right.

For testing in Nim, I recommend a few ways:

  • since it's compiled you can just run things, fire up terminals or a way to observe it -- doing things purely in memory ain't that fast
  • use a when defined(testMode) swap in observable data structures/loggers and get observability that way
  • have a non-void layer and test

@matfournier
Copy link
Author

@saem if you went down this route:

how would you implement layer that also updates layerValue of tkEcho?

type
  TerminalKind = enum 
    tkEcho, tkbear
  ITerminal = object
    layer: proc (i: int)
    layerValue: ptr[int] # this feels super hacky and not GC'd. 
    case kind*: TerminalKind
    of tkEcho:
      id: string
    of tkBear:
      discard

proc newEchoITerminal(s: string): ITerminal =
  var layerValue = 0
  ITerminal(
    kind: tkEcho,
    layerValue: addr layerValue,
    layer: proc(i: int) =
      echo fmt"{i} was passed in"
      layerValue = i, 
    id: s
    )

seems wrong to have to use the pointer but no other way I can think of ti implement layer which also stores a value in layerValue

@saem
Copy link

saem commented Jul 1, 2021

type
  TerminalKind = enum 
    tkEcho, tkbear
  ITerminal = object
    case kind*: TerminalKind
    of tkEcho:
      layerValue: int
    of tkBear:
      discard

proc layer(t: ITerminal, i: int) =
  echo fmt"{i} was passed in"
  case t.kind
  of tkEcho:
    t.layerValue = 13
  of tkBear: discard
  # presuming you don't need a per terminal interface proc behaviour

proc newEchoIterminal: ITerminal =
  ITerminal(
    kind: tkEcho,
    layerValue: 0
  )

@deech
Copy link

deech commented Jul 1, 2021

How's this?

import strformat

type
  TerminalKind = enum
    tkEcho, tkbear
  ITerminal = object
    layer: proc (i: int)
    layerValue:  ref int
    case kind*: TerminalKind
    of tkEcho:
      id: string
    of tkBear:
      discard

proc newEchoITerminal(s: string): ITerminal =
  var layerValue : ref int
  new layerValue
  layerValue[] = 0 # some default value
  proc updateLayerValue(i:int) =
    layerValue[] = i
  result =
    ITerminal(
      kind: tkEcho,
      layerValue: layerValue,
      layer: updateLayerValue,
      id: s
      )

when isMainModule:
  let t = newEchoITerminal("echo terminal")
  echo fmt"Should be 0: {t.layerValue[]}"
  t.layer(1)
  echo fmt"Should be 1: {t.layerValue[]}"

@matfournier
Copy link
Author

@deech:

the simplest record of functions appears to be the easiest?

type 
  TerminalFunctions = object
    layer: proc(i: int)

proc echoTerminalFunction(): TerminalFunctions = 
  TerminalFunctions(layer: proc (i: int) = echo fmt"layer was {i}")

proc recordingTerminalFunction(layerValue: ref int): TerminalFunctions = 
  proc updateLayerValue(i: int) = 
    layerValue[] = i
  result = TerminalFunctions(layer: updateLayerValue)

when isMainModule:

  echo "trying recordinfFunctions"
  let echoTerminal = echoTerminalFunction()
  echoTerminal.layer(1)
  var layerValue: ref int
  new layerValue
  let recordLayerTerminal = recordingTerminalFunction(layerValue) # e.g. for a test
  recordLayerTerminal.layer(10)
  echo fmt"recording terminal layer is {layerValue[]}"

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment