Hello,
We're comparing Nim to java, and are stumped by some design notions and would like some clarification.
For example, Java has the object class, which provided default equals and hashcode methods that objects need to override to get the equality right.
What's the best way to do that in nim? We tried overriding == but we have a couple of issues: 1- we cannot compare two objects that are not of the same type. 2- we're not sure about the order of operation. For example, when we do ob1 == ob2, which == method gets called? 3- the examples tell us to use proc. However, that broke symmetry with the subtype when we added them to a HashSet, which meant we had to use method. 4- using a parameter type object did not work, and the parameter type auto only accepted objects of the same type as the class, which means you need to do extra steps when you don't know the type you're comparing your object with (type checking). Why? Why not return false, or compare the object reference as if == was not overridden?
Example:
Type 1: Person
type
Person* = ref object of RootObj
name*: string
age*: int
method `==`*(this: Person, other: any) : bool {.base.} =
if (not(other of Person)):
result = false
else:
var p2: Person = (Person) other
if (this.name == p2.name and this.age == p2.age) :
result = true
else:
result = false
method hash*(x: Person): Hash {.base.}=
var h: Hash = 0
h = h !& hash(x.name)
h = h !& hash(x.age)
result = !$h
Type 2: Professional
type
Professional* = ref object of Person
specialty*: string
method `==`*(this: Professional, other: auto) : bool =
if (not(other of Professional)):
result = false
else:
var p2: Professional = (Professional) other
if (this.name == p2.name and this.age == p2.age and this.specialty == p2.specialty) :
result = true
else:
result = false
method hash*(x: Professional): Hash =
var h: Hash = 0
h = h !& hash(x.name)
h = h !& hash(x.age)
h = h !& hash(x.specialty)
result = !$h
I can provide examples where proc breaks asymmetry.
Thanks, Abdullah
Well Java has had effectively zero influence on Nim's design and OO textbook examples like yours lost their convincing nature two decades ago.
Here is how I would do it:
import hashes
type
PersonKind* = enum
normal, specialist
Person* = object
name*: string
age*: int
case kind*: PersonKind
of normal: discard
of specialist:
specialty*: string
proc `==`*(a, b: Person): bool =
if a.kind == b.kind:
result = a.name == b.name and a.age == b.age
if result:
case a.kind
of normal: discard
of specialist:
result = a.specialty == b.specialty
proc hash*(x: Person): Hash =
var h: Hash = 0
h = h !& hash(x.name)
h = h !& hash(x.age)
case x.kind
of normal: discard
of specialist:
h = h !& hash(x.specialty)
result = !$h
Written in this style, with exhaustive case statements, you cannot forget to add the corresponding logic when a new kind of person is added to the system. Plus we modelled the mutability aspect properly, equality and hashing cannot mutate Person. You can also store these types in a database without an ORM layer. And we can add new operations without touching the existing code. In the OOP version you can add new subtypes without touching the existing code. But that happens rarely so the OOP version loses.
And we can add new operations without touching the existing code. In the OOP version you can add new subtypes without touching the existing code. But that happens rarely so the OOP version loses.
Also called the expression problem.
Araq:
But that happens rarely so the OOP version loses.
But what if it happens? ;-) Although the Nim approach is kind of straightforward, I find it relatively verbose and I suppose it won't scale well for many variants in the type.
mratsim:
Also called the expression problem.
I knew the problem, but not the name. :-)
Looking at https://journal.stuffwithstuff.com/2010/10/01/solving-the-expression-problem/ as an example, I wonder how you would solve this with Nim in a simple way if you can't rely on the modifiability of the source code of the two original types and operations.
can't rely on the modifiability of the source code
Seems Nim generic can satisfy your requirements, but I wonder how often such openness for extension is needed.
type
TextDocument = object
DrawingDocument = object
SpreadsheetDocument = object
proc draw(doc: TextDocument) = discard
proc draw(doc: DrawingDocument) = discard
proc draw(doc: SpreadsheetDocument) = discard
proc load(doc: TextDocument) = discard
proc load(doc: DrawingDocument) = discard
proc load(doc: SpreadsheetDocument) = discard
proc save(doc: TextDocument) = discard
proc save(doc: DrawingDocument) = discard
proc save(doc: SpreadsheetDocument) = discard
proc loadDrawSave[D](doc: D) =
doc.load()
doc.draw()
doc.save()
loadDrawSave(TextDocument())
loadDrawSave(DrawingDocument())
loadDrawSave(SpreadsheetDocument())
It should be obvious on how to add new methods or adding new types without modifying original source code. Nim can static check if D has load/draw/save inside loadDrawSave in a sense like duck typing.
Thanks!
The missing piece I think would be mixed containers of Document s. I got this far ,
type
Document = ref object of RootObj
TextDocument = ref object of Document
DrawingDocument = ref object of Document
SpreadsheetDocument = ref object of Document
method draw(doc: TextDocument) = discard
method draw(doc: DrawingDocument) = discard
method draw(doc: SpreadsheetDocument) = discard
#[
proc load(doc: TextDocument) = discard
proc load(doc: DrawingDocument) = discard
proc load(doc: SpreadsheetDocument) = discard
proc save(doc: TextDocument) = discard
proc save(doc: DrawingDocument) = discard
proc save(doc: SpreadsheetDocument) = discard
]#
method loadDrawSave(doc: Document) {.base.} =
#doc.load()
doc.draw()
#doc.save()
loadDrawSave(TextDocument())
loadDrawSave(DrawingDocument())
loadDrawSave(SpreadsheetDocument())
let documents: seq[Document] = @[TextDocument(), DrawingDocument(), SpreadsheetDocument()]
for doc in documents:
loadDrawSave(doc)
but I'm not there yet:
Hint: used config file '/playground/nim/config/nim.cfg' [Conf]
Hint: used config file '/playground/nim/config/config.nims' [Conf]
....
/usercode/in.nim(7, 8) Warning: use {.base.} for base methods; baseless methods are deprecated [UseBase]
/usercode/in.nim(8, 8) Warning: use {.base.} for base methods; baseless methods are deprecated [UseBase]
/usercode/in.nim(9, 8) Warning: use {.base.} for base methods; baseless methods are deprecated [UseBase]
/usercode/in.nim(23, 6) Error: type mismatch: got <Document>
but expected one of:
method draw(doc: DrawingDocument)
first type mismatch at position: 1
required type for doc: DrawingDocument
but expression 'doc' is of type: Document
method draw(doc: SpreadsheetDocument)
first type mismatch at position: 1
required type for doc: SpreadsheetDocument
but expression 'doc' is of type: Document
method draw(doc: TextDocument)
first type mismatch at position: 1
required type for doc: TextDocument
but expression 'doc' is of type: Document
expression: draw(doc)
(Also, I'm not sure what to make of the missing {.base.} in this context, since I want to work with a specific derived type.)
How about this?
# -------------------------------------------------------------
# this part of code does not change
type
TextDocument = object
DrawingDocument = object
SpreadsheetDocument = object
proc draw(doc: TextDocument) = echo "draw text"
proc draw(doc: DrawingDocument) = echo "draw drawing"
proc draw(doc: SpreadsheetDocument) = echo "draw spreadsheet"
proc load(doc: TextDocument) = echo "load text"
proc load(doc: DrawingDocument) = echo "load drawing"
proc load(doc: SpreadsheetDocument) = echo "load spreadsheet"
proc save(doc: TextDocument) = echo "save text"
proc save(doc: DrawingDocument) = echo "save drawing"
proc save(doc: SpreadsheetDocument) = echo "save spreadsheet"
proc loadDrawSave[D](doc: D) =
doc.load()
doc.draw()
doc.save()
loadDrawSave(TextDocument())
loadDrawSave(DrawingDocument())
loadDrawSave(SpreadsheetDocument())
# -------------------------------------------------------------
# define a process for open type T
proc process[T](docs: openArray[T]) =
for doc in docs:
doc.loadDrawSave()
process([TextDocument(), TextDocument()])
process([DrawingDocument(), DrawingDocument()])
process([SpreadsheetDocument(), SpreadsheetDocument()])
# -------------------------------------------------------------
# define an interface for the process
type
# define an interface for the process
LoadDrawSavable = object
loadDrawSave: proc(): void
# declaration of Document is for convenience only
Document = TextDocument | DrawingDocument | SpreadsheetDocument
# convert types to conform the interface
proc toLoadDrawSavable(x: Document): LoadDrawSavable =
result.loadDrawSave = proc() = loadDrawSave(x)
process([
TextDocument().toLoadDrawSavable,
DrawingDocument().toLoadDrawSavable,
SpreadsheetDocument().toLoadDrawSavable
])
# -------------------------------------------------------------
# now extend one more type of document
type
PresentationDocument = object
proc draw(doc: PresentationDocument) = echo "draw presentation"
proc load(doc: PresentationDocument) = echo "load presentation"
proc save(doc: PresentationDocument) = echo "save presentation"
proc toLoadDrawSavable(x: PresentationDocument): LoadDrawSavable =
result.loadDrawSave = proc() = loadDrawSave(x)
process([
TextDocument().toLoadDrawSavable,
DrawingDocument().toLoadDrawSavable,
SpreadsheetDocument().toLoadDrawSavable,
PresentationDocument().toLoadDrawSavable
])
Instead of Document as the typeclass, why not LoadDrawSaveable
type
LoadDrawSaveable = concept x
x.load()
x.draw()
x.save()
#Document = Text | Drawing | Spreadsheet
Perhaps with the restriction already in the original
proc loadDrawSave[D:LoadDrawSaveable](d:D)=
with d:
load
draw
save
Then adding a type is trivial, any type that has those three procs will match, and without the extra lookup and closure overhead Um, would this allow adding new methods without modifying LoadDrawSaveable (which then should have a more generic name)?
Sorry if I come across as picky here. mratsim brought up the expression problem and I'm just curious if both "dimensions" (independently adding types and adding operations without modifying the original code) can be implemented in Nim in an easy way. Of course I'm hoping I never need this. ;-)