I wrote some simple code like this:
var v = 100
proc build_fun(a: int): proc (): int =
var
b = 20
proc foo(): int =
b = b + 1
result = a + b
result = foo
var
f = build_fun(10)
f2 = build_fun(10)
echo f()
echo f()
echo f2()
echo f2()
What I like about TypeScript is that I don't have to give build_fun a type at all. I somehow think that the best code is the one you don't have to write.
And even Haskell introduced type holes at some point meaning you can type parts of a function and let the compiler figure out the holes.
How do you feel about this ?
Overloading plus Hindley-Milner like type inference is NP-complete and Nim focuses on overloading. I have never regretted this decision in my life. ;-)
More type inference is not necessarily good: "I have to type less" has to be weighted against "error messages are more mysterious than ever".
Part of the question is also whether how close Nim is to bidirectional typing cause I've been dreaming about that for a long time.
It's far away.
"Overloading plus Hindley-Milner like type inference is NP-complete and Nim focuses on overloading."
Thanks for explaining it, I always wondered what was the reason of the inference "lacking". Is there a paper/resource you can link where the relationship is explained in more detail?
On the other hand, does the statement above includes being able to infer three parametric types (i.e. std/options.flatMap)?
}
It allows to reuse things from scope easily minimizing passing arguments.
And from my point of view its fine if the compiler can infer the return type without having me to think about it or write it. But maybe its a very special case.
I agree about mystic error messages in some cases.
In the TS case it allows async initalization classes don't.
Overloading plus Hindley-Milner like type inference is NP-complete and Nim focuses on overloading. I have never regretted this decision in my life. ;-)
very interesting
Is there a paper/resource you can link where the relationship is explained in more detail?
From a very quick google search I was able to find this (there might be better references): http://web.cs.ucla.edu/~palsberg/paper/dedicated-to-kozen12.pdf
They prove that overloading for an example language (some type of lambda calculus, that in particular has pretty extensive type inference from what I can tell) overloading is NPI complete. In a language like Java where you can infer type statically (and for this context, I think it applies also to Nim) they have an informal proof that overloading becomes polynomial. And they mention in the end that the the case of ML and Haskell (Hindley-Miller polymorphism whatever that is) the type system becomes undecidable.
While I like optionality and infer for all types, it may be too hard to implement, it's unrealistic.
But what's definitely possible and annoys most is lack of auto types when specifying it explicitly don't contribute to readability or correctness so those types are just annoyance and useless noise. For cases like a) lambda arguments and return types, b) auto casting @[Parent(), Child()] or let list: seq[Parent] = @[Child()] and couple of similar cases. Those cases don't require any extraordinary compiler logic.
That would be nice to have, these cases don't require any extraordinary compiler logic and won't slow down compilation.
You need to prove that by submitting a pull request. :P
lambda arguments
It's supported via std/sugar.
import std/sugar
proc invoke(callback: (x: int, y: int) -> int; x, y: int): int =
callback(x, y)
echo invoke((a, b) => a + b, 1, 2)
Try this:
import std/sugar
proc each(list: seq[int], callback: proc(v: int): void) =
callback 0
@[1].each((v) => echo v)
I do have to say let list: seq[Parent] = @[Child()] working is an odd concept. Just cause generic parameters are convertible does not mean the underlying type is. Consider the following:
type
Parent = ref object of RootObj
Child = ref object of Parent
MyType[T] = object
when T is Child:
a: int
else:
a: (int, int)
MyOtherType[T] = distinct int
var myTyp: MyType[Parent] = MyType[Child](a: 100)
var myOther: MyOtherType[Parent] = MyOtherType[Child](100)
This means that for these implicit upcast conversions you need to traverse the type, and see if the reason they're incompatible is only because of an upcastable type. Otherwise you only specialist for string and have very odd inconsistent type language since var a: Table[string, Parent] = {"bleh": Child().}.toTable does not workThere is nothing impossible here, I am just rambling about how something simple is a bit more complicated than it first appears.
Noble, but futile. Can easily be argued against with the good old "but but but TypeScript!" argument. ;-)
"Pretty much the same thing", that's just wrong. When you do mySeq.add Child() the compiler sees that it can implicitly upcast to the Parent type since it's a mismatched parameter. When you do let mySeq: seq[Parent] = @[Child()] there is no converter for seq[Child] to seq[Parent]. Which means the compiler needs to understand how to convert generic instances between each other and ensure it's safe.
No, the opposite impossible to do even theoretically.
"Better to remain silent and be thought a fool than to speak and to remove all doubt."
It's not impossible. You have RTTI this means you can see if in the places where the generic specialization occurs can be converted down to the desired type. So in the case of the table you'd have to iterate over all instances of Parent and see that every one is instance of Child.
To prove this:
import std/tables
type
Parent = ref object of RootObj
Child = ref object of Parent
converter toDerived(t: Table[string, Parent]): Table[string, Child] =
for x in t.values: # The compiler needs to know how to do this
if not(x of Child):
raise (ref ObjectConversionDefect)(msg: "Could not convert table to derived")
cast[Table[string, Child]](t)
var
passConv = {"bleh": Parent(Child()), "meh": Parent(Child())}.toTable
failConv = {"bleh": Parent(Child()), "meh": Child()}.toTable
childTable: Table[string, Child] = passConv
childTable = failConv
I meant impossible to do statically, of course it's possible to check instance type at runtime.
proc some(parent: Parent): void =
# impossible to determine if parent has specific child type at compile time
compared to
proc some(child: Child): void =
# trivial to cast to Parent
let parent: Parent = child
So, it's trivial to go in one direction, and impossible to go (at compile time, when compiler works) in another.
It works if you convert the first element explicitly.
type
Parent = ref object of RootObj
i: int
Child = ref object of Parent
f: float
let list: seq[Parent] = @[Parent(Child()), Child(), Child()]
echo list[0][]
It's just another form of the following problem:
let
s1: seq[int32] = @[1, 2, 3] #error
s2: seq[int32] = @[int32(1), 2, 3] #works
More type inference is not necessarily good: "I have to type less" has to be weighted against "error messages are more mysterious than ever".
Minimal type inference is one of the better features of Nim, it makes it easier to work with random code.
The point is just cause two generic parameters are related does not mean the data type should support this conversion. seq[Child] -> seq[Parent] is just a single instance of where it can be fine. I can provide a multitude of examples where this implicit conversion causes runtime issues cause it's not desired. This means that one would special case only seq[T] conversions, and even then there are cases likely where one would not want this behaviour.
type MyChannel[T] = object
data: ref seq[T]
proc send[T](channel: var MyChannel[T], data: T) =
channel.data[].add data
type
Parent = ref object of RootObj
someField: float
Child = ref object of Parent
a: int
b: string
# Using your provided logic the following should be done implicitly by the compiler
# Where generic instances are convertible in the case that the generic parameters are implicitly convertible.
# `Child of Parent` holds so this should happen.
converter toParent(channel: MyChannel[Child]): MyChannel[Parent] = cast[MyChannel[Parent]](channel)
var myChannel = MyChannel[Child](data: new seq[Child])
proc handle() =
for x in myChannel.data[]:
echo x[]
var myAlias: MyChannel[Parent] = myChannel
myAlias.send Child(someField: 120, a: 100, b: "hmm")
myAlias.send Parent(someField: 300)
myAlias.send Child(someField: 32, a: 320, b: "huh")
handle()
Do not get me wrong there is some merit to converting generic instances to their parent types, but that's why we have converters and concepts.
type
Parent = ref object of RootObj
someField: float
FromParent = concept fp
fp of Parent
Child = ref object of Parent
a: int
b: string
converter toParentSeq(s: seq[FromParent]): seq[Parent] = cast[seq[Parent]](s)
converter toParentSeq(s: var seq[FromParent]): var seq[Parent] = cast[seq[Parent]](s)
type MyNewChild = ref object of Child
var a: seq[Parent] = @[Child()]
a = @[MyNewChild()]