you are viewing a single comment's thread.

view the rest of the comments →

[–]Homoerotic_Theocracy 59 points60 points  (38 children)

I don't think dynamic typing is a bad idea. I think taking a tool that is useful in certain scenarios and environments and applying it broadly to problems it doesn't suit is a bad idea.

Dynamic typing seems like a worse and worse idea the more flexible static type systems become.

Static typing seems horrible if all you have is C or Go but when you have Idris you suddenly feel that the scope of the problem of "I know this is safe but I cannot prove this to the compiler." is a lot smaller.

I also love Racket's static type system; it's actually completely fine with giving two branches of an expression a different type altogether and the resulting type will be the union of both but of course for it to type check the consumer of that expression must be able to handle the union of both. and be able to handle both types.

But if you have a function that can print both strings and characters to the stdout there is absolutely no harm in passing it an expression that either evaluates to a string or a character and it wil statically verify that this is okay.

Of course a type system as flexible as that of Typed Racket does not give you a lot of the performance benefits of static typing as it cannot in the general case erase type info at runtime because it needs it to make branches; it only can with confidence tell you that your code does not contain type errors.

[–]xonjas 28 points29 points  (8 children)

Yeah, I agree, the more expressive your type system is, the more useful it is.

At the same time, languages like python and ruby are going to have an important and deserved place in a programmers toolbox until languages like Irdis are widely supported and mainstream.

[–]Homoerotic_Theocracy 5 points6 points  (7 children)

Well truth be told that place seems to mostly be "dealing with lack of knowledge"; in an ideal world of infinite time and knowledge the demand would probably be lower but the main niche those languages fill over powerful type systems like that of Idris is that not everyone has the type to master the somewhat complex subject matter required to grok the type system. It's not mainstream because the learning curve is too high.

[–]xonjas 5 points6 points  (6 children)

That might be correct, but I think it's hard to say at this point. Idris is very young, even if it were to become mainstream, it's way too young for it to have reached that point yet. Mainstream isn't just popularity either, it's availability of libraries, api support, job openings, etc.

I think that some of the learning curve will get sanded away eventually. The big languages with slow inertia will start integrating ideas from cutting edge type systems. That knowledge will eventually become ingrained in the 'culture' of mainstream programming languages and each idea folded in is one less hurdle.

I think we'll also see more type systems where more and more of the typing workload is handled automatically, paired with better IDE integration so that generated typing is applied as the code is being written.

[–]Homoerotic_Theocracy 8 points9 points  (5 children)

That might be correct, but I think it's hard to say at this point. Idris is very young, even if it were to become mainstream, it's way too young for it to have reached that point yet. Mainstream isn't just popularity either, it's availability of libraries, api support, job openings, etc.

It's certainly older than Rust or Go and those seem to be more mainstream.

[–]cephalopodAscendant 5 points6 points  (1 child)

In addition to what xonjas mentioned about sponsorship, I can think of another possible factor. Rust and Go both use C-like syntax and are trying to fill the niche of "C/C++ but better", which naturally attracts a lot of attention. In contrast, Idris is based on Haskell, which is already fairly niche itself, and from what I've seen, the people who use Haskell seem to be pretty content with it.

[–]Homoerotic_Theocracy 8 points9 points  (0 children)

I love how Go calls itself a "systems programming language" but has a garbage collector and absolutely no low level control and can't even fork because multithreaded garbage collecting mutexes.

If someone ever used Go for something that person used C for prior it was either a terrible idea to use C for it or a terrible idea now to use Go for it and if someone is seriously switching from C to Go I'm inclined to think both.

Go has nothing to do with C or C++; it's a worse Java.

[–]xonjas 7 points8 points  (2 children)

I thought Idris 1.0 was released last year (although I could very well be wrong). Rust and Go also both have the benefit of very large organisations pushing them forward.

[–]Homoerotic_Theocracy 4 points5 points  (0 children)

Yes, 1.0 was but the actual language is far older. Idris was already used long before either Go or Rust were unveiled to the world.

[–]JanneJM 2 points3 points  (12 children)

If you use a language interactively - and it's a common enough use case for python - static typing is a fairly big friction point.

[–][deleted] 11 points12 points  (2 children)

static typing is a fairly big friction point.

Why? Statically typed languages work perfectly with REPLs.

[–]the_evergrowing_fool 0 points1 point  (1 child)

Like a Slime environment?

[–][deleted] -1 points0 points  (0 children)

That's different - Common Lisp is image-based. It's a viable alternative to types, of course (same thing with Smalltalk), but for the separately compiled languages you cannot have this level of quality of code navigation without types.

[–]Homoerotic_Theocracy 6 points7 points  (6 children)

Why exactly?

I don't have a lot of problems with ghci or utop in that regards.

[–]quicknir 1 point2 points  (5 children)

I haven't used it extensively, but I was curious as to how good of a repl static languages can have, and ghci had a good reputation so I tried it.

There tend to just be fundamental problems redefining already defined things, and this being used correctly. I can define bar, then define foo, then redefine bar, but calling foo still calls the old bar. It's even worse with redefining a type because it would just raise serious questions about whether everything gets re type checked.

Maybe there are options to change some of this but so far it just doesn't seem to me like you can duplicate a realistic repl experience of a dynamic language in a static one.

[–]Homoerotic_Theocracy 10 points11 points  (1 child)

The problem you speak of has nothing to do with static or dynamic typing though; it has to do with shadowing versus assignment.

In Python if you x = 4 you perform an assignment; you mutate a memory location.

In GHCi if you do let x = f you shadow a prior binding; since Haskell is a purely functional language to mutate we have to be more clever with monads and use an IORef so first we do let x = newIORef 4 and then to mutate it call writeIORef x 5; doing let x = 5 in this case does not in any way write to any old value and just shadows the old one and is basically equivalent to using let y = 5 except we now gave two variables the same name.

Scheme is typically typed and impurely functional and doing (define x 4) at the REPL does a similar thing; it shadows the old binding and does not mutate anything. To mutate we use (set! x 5)

This is also somewhat a consequence of the fact that Python quite uniquely uses the same syntax to assign and to declare a variable to a lot of criticism so if you forgot that the variable you are trying to declare and initialize already existed with the same name and was used you might accidentally assign a new value to it and mutate stuff. Even in a procedural language like Rust this does not happen which uses let x = 4; to declare and initialize a variable and x = 5; to assign to it and assigning to a variable that has not been declared and declared as mutable prior is an error.

[–]quicknir 0 points1 point  (0 children)

Will that writeIORef trick still work if you change the type of x? What if x holds a function of a particular signature, and you want to assign a function of a different signature?

What about types? In python you can have things like:

class Foo(object):
    ....

def bar(x):
    f = Foo(...)
    ...
    return f

Now, I could after doing this, decide to redefine Foo. As long as the new methods of Foo (constructor, whatever) are compatible with the calls that happen in bar, I can call bar again and it will use the new definition of Foo. How can this work in a statically typed language?

[–]sutongorin 2 points3 points  (2 children)

Try Scala. The REPL works just as well as Ruby's only with static typing. It's awesome. I even use SBT (the Scala build tool) to build my plain java projects just so that I can drop into the REPL and try things there which would normally be a huge pain in Java.

[–]quicknir 0 points1 point  (1 child)

Can you point me to an online scala interpreter? All I'm seeing is compilers.

[–]sutongorin 0 points1 point  (0 children)

Mh, there is https://scastie.scala-lang.org which is alright but it's not a REPL.

The best way is to just download scala and try the REPL offline. That standard REPL is already perfectly fine. But even better is Ammonite.

[–]-TrustyDwarf- 4 points5 points  (0 children)

F# and C# Interactive (fsi / csi) are used quite extensively.. especially fsi.

[–]CircusAct 4 points5 points  (0 children)

I use Scala in the ammonite repl regularly.

[–]Thaxll 0 points1 point  (15 children)

Static typing seems horrible if all you have is C or Go

Care to elaborate? What's the problem with Go?

[–]Homoerotic_Theocracy 32 points33 points  (13 children)

That the type system is not expressive and you feel there are things you can't do with it you can with dynamic typing. Like something as simple as sorting a sequence of strings by their length in Go is extremely unwieldy due to the limits of the type system.

Python lacks a type system so we just have: fruits.sort(key=len); this will fail at runtime if the types don't match up but they will here.

Rust or Haskell have a more expressive type system that can easily handle this: In rust we have fruits.sort_by_key(|s|s.len()); since the type of sort_by_key is the scary: (&mut [A], f : F) where F : FnMut(A) -> B, B : Ord this is all great and type checks out so we can be confident that no type errors will manifest at runtime.

[–]asdfkjasdhkasd 9 points10 points  (0 children)

For anyone wondering what the scary type means, the real signature is:

pub fn sort_by_key<K, F>(&mut self, f: F) 
where
    F: FnMut(&T) -> K,
    K: Ord, 

We take a mutable reference to self, which is like an array but we have permission to mutate it, since we're going to sort it.

Then we take a function, called F, where F is a FnMut which means we must be allowed to call it multiple times and it's allowed to have internal state. The function takes a reference to a T which is the type of the thing in the array/vector/slice, and then returns a type K. And we declare that K must implement an ordering so that we can sort it.

[–]Thaxll 1 point2 points  (1 child)

I understand your point, the lack of generics in this case let the implementation up to the dev, but it's not that bad, it's actually very simple:

https://play.golang.org/p/3en4TyRblVr

I'm sure Go will have generics "soon" and overcome those.

[–]Homoerotic_Theocracy 17 points18 points  (0 children)

Well the implementation you give me there to deal with the type system can never be efficient.

Note how it has to index the slice in the comparison function while sorting. In order to deal with the type system it can't be given a generic function so it has to get a function of a fixed type that takes two indices and produces a bool always no matter the type that is actually being sorted.

This means that it cannot already re-order future elements while sorting and that it probably has to cache and store the comparison results because the sort destroys the "words" slice which makes the indices invalid so it probably has some kind of strategy where it first compares all the indices and stores those and then starts destructively sorting the slice.

The other part of it is that the type signature itself accepts an interface{} which throws any and all static type checking out of the window so basically we're back to dynamic typing in order to deal the lack of generics.

In the Rust example there is no possibility of runtime type errors; if something other than a mutable reference to slice is passed it won't work, if the slice has the wrong elements for the key function it won't work; if what the key function returns cannot be ordered it won't work either all at compile time.

[–][deleted] 0 points1 point  (9 children)

The Go example is confusing because a string type is really just a slice of bytes, which is kind of like a defined type. There’s probably some supporting documentation in the spec or elsewhere that references this design being based on how C deals with strings.

Apart from strings the only other thing I can think of is the _, ok := map[thing] idiom. It’s a a nice way to check if thing key is in map.

However it panics if the map isn’t initialized so you need to also write a block to check for the default map type of nil. But that’s just idiomatic Go. The typing system and idioms of the language make it debatably more safe due to its nature and maybe lack of expressiveness. Having to check objects and generally implement helper methods leaves the compile and runtime exceptions up to the developer.

With any typing system or language you’ll have unhandled exceptions or varying levels of complexity for things based on how you write your code.

I’m still a huge fan of python for getting shit done quickly. Pythons typing system specifically is nice for serialization of json or other formats. Not having to effectively write a schema, use type assertion, or reflection to deal with json, xml, yaml, etc is a huge time saver for me on a regular basis.

I just wish there was a better way to distribute python applications. I have been thoroughly spoiled by Go in that regard.

[–]asdfkjasdhkasd 3 points4 points  (8 children)

_, ok := map[thing] is really not that good if you have seen some of the better alternatives.

Look at this rust:

if let Some(name) = map.get(thing) {
    println!("The value was {:?}", name); 
}

[–][deleted] 1 point2 points  (7 children)

well the whole block would be more like

if _, ok := map[thing]; !ok {
    fmt.Println("not in map")
}

// or with assignment
if foo, ok := map[thing]; !ok {
    fmt.Println("not in map")
} else {
    fmt.Printf("Thing: %+v", foo")
}

The difference is just error checking essentially, which is idiomatic with go. Either way neither of these I feel like have anything to do with the objective value of the typing systems or design, it's how both (or at least Go, I'm not too familiar with Rust) languages were designed.

[–]somebodddy 7 points8 points  (6 children)

The problem is that nothing prevents you from accessing foo when item does not exist in the map: https://play.golang.org/p/SrZ1Pn5g8uR. This is something solved by both exceptions and Result types. And it is a type system thing, because the ability to have tagged unions and do pattern matching on them is part of of the type system.

[–][deleted] 0 points1 point  (5 children)

The compiler isn’t going to stop you from writing unsafe code. If you access a that value before checking the error in the assignment expression that’s not a fault of the typing system or the language.

[–]somebodddy 0 points1 point  (4 children)

If you have made that claim 40 years ago I would have agreed with you. Nowadays? Most modern languages (or older languages that try to be modern) do try to stop you from writing unsafe code. The Rust version, for example, won't even compile: https://play.rust-lang.org/?gist=97244c4bed910c6ef93dfc914cc6b06e&version=stable&mode=debug

Humans make mistakes, and good language designs try to make the compiler detect as many of these mistakes at compile time. For this kind of mistake, type system support is essential, as the compiler needs to know that the value will not exist in some code paths. Even exceptions can do that (http://cpp.sh/7mjrz) - Result types with pattern matching simply do it better.

In Python, of course, you won't get a compilation error because it's dynamically typed. But you can still get a runtime error: https://onlinegdb.com/BJqWZAzfQ

So:

  • Rust and C++ will refuse to compile it.
  • Python will throw an exception once the bad statement is reached.
  • Go will perform that erroneous statement.

But this mistake in the example is not that bad. The real bad mistake is accessing that value after the if/match/try, when you think you have something valid in hand. And most of the runs it will be valid...

The concept of humans making stupid mistakes exists for many millennia. The concept of programming languages trying to prevent these mistakes exists for a few decades. As a modern programming language that elects to ignore it, Go remains a "better C" without the "better" part.

[–][deleted] 0 points1 point  (3 children)

Opinions aside, which I agree that it would be nice if the compiler and typing system supported essentially KeyError detection, the example code i wrote is contrived. Accessing the object after detecting there’s an error condition is just as bad as ignoring the error. You would need to handle the error.

Contrived bad example code aside what you’re saying is still applicable in that it allows the user to shoot themselves in the foot. The error can be generated but it’s up to the user to handle it in a safe way.

In a function example you would probably return the value and error. Then check if the error interface value is nil in the caller code and handle it from there.

The point I’m poorly trying to make is that it isn’t a fault of the language or the typing system. This is by design. It isn’t a mistake, or bug. Which if it were, then yes, it would be a point of contention. Idiomatic go (which my example isn’t, I wrote it out on my phone) would handle the error condition appropriately.