ossu / computer-science

🎓 Path to a free self-taught education in Computer Science!
MIT License
172.19k stars 21.73k forks source link

How To Code: Simple Data thoughts and observations #544

Closed KathrynN closed 5 years ago

KathrynN commented 5 years ago

I'll start with the pros, because there are some! It's clear the tutors worked very hard on the course and making it accessible.

Pros

Cons [NB some of this is very subjective, and I haven't yet finished the course. I had intended to so before I wrote this ticket, but after discussion with @hanjiexi I figured I may as well share now]

[Disclaimers: I am but one person, and the views expressed here are personal to me. I have not yet completed the course. I am not a beginner, although I am a beginner to computer science. I also sometimes exaggerate. I would not be exaggerating, however, in saying that I find the course incredibly frustrating] @gjergjk71 - anything to add?

joshmhanson commented 5 years ago

Thanks for posting all your thoughts! You were thorough, so I will be thorough too.

The code is ugly and hard to read. There are parentheses everywhere, both ) and ] but, surprisingly, no } (yet)

Yeah, S-expressions are a low-level syntax. You see them in the intermediate languages of many compilers, in WebAssembly, and commonly as a data serialization format. If we could have the exact same course that used a higher level syntax I would pick it in a heartbeat. I know one of the HtDP authors is working on one such language and that's likely where we're headed whenever there's more material available for us to use.

One advantage, however, of this lower-level syntax is how close it is to lambda calculus syntax, which (in my planned changes) students would be learning about soon after the course finishes. I think the advantages outweigh the disadvantages.

All of that said, I adopted a style for S-exp based languages (involving lots of newlines with vertically aligned columns) that I ended up preferring over everything else — even other languages.

The instructors insist on using naming conventions that, should they pass through my pull requests, I would reject outright. In few worlds outside of embedded are variables in functions now just one letter, so why can't we have (define (name-cat name) ...) instead of (define (name-cat n) ...)? Why name a constant (define P1 0) when you could name it (define START 0).

I agree with this; I ended up renaming some of the provided variables to make them easier to remember.

  • There is an overreliance on course design schemas that can only be found in the course, that are overly complicated, and that would be difficult for a beginner to learn. I shouldn't have to return to a design pattern five times when designing a "world" - once for the world, three more times for templates... it's just repetitive and lacks value
  • Over-reliance on copy-pasting. In an almost flagrant disregard of DRY (do not repeat yourself) principles, the instructors have us copy-pasting templates all over the place, even when they add little to no value and make the solution more complex.
  • Over-reliance on function calls. Why, to make a list, must I call (cons 1 (cons 2 (cons 3 (cons 4 empty)))) instead of just (make-list 1 2 3 4) or similar?

I grouped these together because these are all really just the beginner-oriented pedagogical approach at work.

Beginners need to see the same patterns over and over again to ingrain them into their minds, so copy-pasting templates is directly beneficial. It's no different from a math teacher recommending that students copy down the formula for each new problem that they will use it to solve.

DRY (i.e. abstraction) comes later, when you know the patterns by heart, at which point you can use functions that abstract over those patterns. BSL deliberately doesn't include these, but ISL and ASL (used later in the course) provide many such abstractions, such as list-literal syntax (list 1 2 3 4). See the language progression here: https://docs.racket-lang.org/htdp-langs/beginner.html

No variables, only constants. This makes programs far more bracket-heavy than they need to be, and mean that simple tests and function definitions are dragged out over multiple lines

Not quite sure what you mean, are you talking about mutable data? If so see my next section.

No objects. You define structs, but these are essentially read-only collections of data. They are completely immutable. A function that needs to take a Cat and return a Cat must make an entirely new Cat. In object-orientated languages, however, the student would be able to simply call cat.setXPosition(cat.x + SPEED) and have done, compare with (make-cat (+ (cat-x cat) speed) (cat-y cat) (cat-age cat))

What you're describing isn't a difference between records/structs and objects, but a difference between records with and without mutable fields.

You mention "simply call cat.setX...`" but this is far from simple. Introducing mutation means completely re-designing the teaching material around a different and more complex model of computation. The substitution model is used here because it is the simplest starting point (they also use it in the beginning of SICP). It's the same model students would have been accustomed to in high school math. However, this model breaks in the presence of mutable data.

Plus, immutability is actually quite normal nowadays due to how mutation doesn't scale easily to large or complex systems. In React(+Redux) for instance, you are treating data as mostly immutable, with functional reducers for handling state and components as pure functions of the state. The React(+Redux) engine then handles the mutation by diffing the new structures against the old.

As an aside, objects occupy a layer of complexity above records/structs. Records are just regular product types. But an object is a kind of extensible record that somehow has access to self/this. How do you extend a record? You generally you need either subtyping or row polymorphism. While you could introduce objects in a beginner class I think it creates too much confusion and cognitive overload. Plus, almost every language tightly couples its object system with its class system, which is even more complexity.

Boundaries on student-defined data structures. We are essentially writing in a contract and then assuming that whoever calls it will obey this contract, but there is no code enforcing this behaviour. There is therefore nothing to stop the student from missing a case where they accidentally provide or return the wrong data type. (NB this is a problem with teaching untyped languages to beginners in general, and is not unique to student language))

All true. I wrote about the trade-offs of static vs. dynamic typing in a beginner course in #540. I find language-level dynamic typing to be silly in general — it is subsumed by static typing — but I ultimately felt that dynamic typing (or even no typing at all) is better suited for beginners.

But this is probably the only course where I feel comfortable with this, because if you pay careful attention, they are actually teaching type-driven programming with a simple algebraic type system. It's just that, for the sake of simplicity, nothing is verifying those types; they're going in the comments above each function. This provides the foundation for moving to static typing later.

If there is a way to make a zero-argument function I couldn't find it by trial and error (now that I've written this, someone is going to point out it's super easy), and the case isn't covered by the instructors. Therefore you simply can't implement generators without having them take arbitrary data that they then ignore - something else that would get a stern look in any code review.

I've never heard of a zero-argument function; if it has no arguments it would just immediately evaluate the right hand side to a value. To delay evaluation of the right hand side (i.e. to turn a value into something that can be called) you need an argument. An argument with no data is called the unit value and is written ().

Just as calling a function that takes unit is written some-fun () in most languages, in Lisp/Racket/BSL you just move the left parenthesis to the left of the function name, yielding (some-fun). You can make multiple calls to the same function this way, e.g. two calls would look like this: ((some-fun)).

But anyway, BSL is a deliberately limited language and perhaps doesn't support whatever you're doing. (Lambdas don't come until ISL.) I don't think any of this should be necessary in the course. I don't recall ever defining functions that took unit, and they would have taught the student everything needed to solve a certain problem. I do recall being challenged around generating something, but I eventually found a solution without doing anything fancy. I think I often experienced what you are: trying to solve something in the way I'm used to and struggling, because the problems are designed for people with a much more limited set of options and experience.

BSL - the language, despite being ugly and full of parentheses) is definitely not equipping the student to find gainful employment, nor the tools to move to e.g. javascript, java, python.

This is where OSSU differs significantly from coding bootcamps and other similar short-term programs. OSSU is meant for long-term study of several years, similar to an undergraduate degree. The goal is to provide the right level of difficulty at the right level of abstraction for students in a multi-year journey from novice to master. You don't have time for proper scaffolding and robust conceptual understanding in a short-term program.

Students will end up learning those messy industrial tools later in our curriculum, but learning simple and elegant models of computation first will give them a significant competitive advantage compared to their peers in the bootcamps. We can't compete with bootcamps on quantity, so we compete on quality.

xxylem commented 5 years ago

Hi,

First some quick background:

Started OSSU December 2017 with MIT Intro course, then CS50, then How To Code. Currently knee-deep in maths and in the middle of the Haskell from First Principles book. Do about 10-12 hours a week normally.

Had very little CS knowledge prior to starting, maybe a little bit in python here and there from online courses, vaguely knew about types, not much else.

Obviously it's been some time since I did these courses, but I'll add my point of view in case it's helpful.

I liked the MIT and CS50 courses but came out of them a bit frustrated. I did CS50 till the end (despite the warning) and found the idea that for the final assignment I could just go and write a program of anything I liked in any language I liked a bit unrealistic. It's like teaching your student how to say "hola" and "adiós" and expecting them to go and have an in-depth conversation in Spanish.

I came away from them having a good idea of how to write a function that does some action so that I could solve the (mostly-logic) problems in the assignment (e.g. print blocks to the screen in a particular pattern). I didn't really know how to put these small parts together and get them to speak to each other in a particularly meaningful way. Nor did I know how any of the stuff I was writing was being effected by the computer.

That's OK of course; they're introductory courses. They're there to pique interest and present options/possibilities for further investigation/study. They should provide more questions than answers.

How to Code was useful as the next step. It gave me what I wanted: a way to reason about the design of programs, moving away from individual actions. In particular, I liked that I moved from typing and praying (failing tests without know why; making arbitrary changes to code to try and pass tests) over to thinking about what I want first and how I can express what I want to do using predictable patterns. It gave me a way to think about what I was doing.

That being said, I don't think it should be the very first course. It lacks the excitement of the other two courses. It's mostly a guy talking to you in a fairly flat tone. You need to be already a bit interested/invested in learning the material I feel.

To me, the curriculum works pretty well as is. MIT Intro/CS50 hook you in. How to Code gives you structure/methodology to follow, then Programming Languages gives you a solid theoretical foundation.

===============================================================================================

Regarding some specific points made earlier in the thread:

Parentheses: Agreed, an eyesore.

Copy-pasting: I found that as I did a particular pattern a number of times, the pattern or idea behind it would be internalised. I would stop copy-pasting and start saying, OK, first I need to think about what type this function takes, and what type I want it to return, and so on until the function/feature is written fully.

No objects: I had the opposite problem, e.g. it took me ages to get my head around the idea of 'this' and inheritance. When I did the MIT Intro I was just adding 'this' here and there until tests passed, without really understanding what was happening, or how my data was represented.

Cons cells: I don't really like the names cons, car, cdr, etc, though I think it's important to break a list into its underlying form to get the idea of recursion and how to operate on data that has this kind of form. With a list like (make-list 1 2 3 4) it's less apparent.

===============================================================================================

The pace of the course is the main problem for me: it's very slow and steady. The pacing is useful to build up good working habits and methodology, but I can completely understand why people dropped it - I'm pretty sure I almost did too! It was definitely worth it for me to stick it out. But I can't see it as the first program in the curriculum. If it's a few courses in and people drop it because it's not for them, they've already invested some time and will look for a more suitable course. If it's the first course, if they drop it, they might well drop the whole subject thinking it's not for them.

Anyway, let us know if I can help in any way. Love that you've put all this together, it's been great fun to study.

Regards,

Josh

joshmhanson commented 5 years ago

Started OSSU December 2017 with MIT Intro course, then CS50, then How To Code. Currently knee-deep in maths and in the middle of the Haskell from First Principles book. Do about 10-12 hours a week normally.

That is awesome @xxylem. Keep going!! Thank you for coming here to share your thoughts.

There is nothing quite like How to Code / How to Design Programs in terms of its highly structured approach, and that's why I can't in good conscience replace it with something similar but more challenging. Plus, we'll find that more challenging material often has many of the same perceived deficiencies (Lisp syntax, cons, etc.), or if it uses something else it will have a different set of deficiencies (e.g. weird Pythonisms).

Your story led to me think about how different the experience of OSSU is for everyone, and how there is nothing I can really do to stop it. There is another criterion for evaluating a course that I have a tendency to neglect, which is emotional impact. No matter how theoretically excellent a course's content is, it would be very sad if the emotional impact of taking it as a first course was so bad that the student ends up quitting. So I'm going to re-think my approach in #540.

@KathrynN, I'll leave the issue open for a short while in case you have more thoughts to add about later parts of How to Code. I do hope you continue with it and start to see the benefits, but feel free to share either way. I prefer negative feedback over no feedback!

jestarray commented 5 years ago

I took this course 2 or so years ago? Before then I had some experience making prototype games in javascript. Very good points! I dropped it half way through our so(at abstraction) because I felt like I got the general gist of its all about. For people who have had some programming experience, it does get dredging.

Anyways, some objections!

I personally think the syntax is very intuitive and the language is great for beginners. I took Java with friends who had zero programming experience and helped a little with C++ and I've found they're just blindly bashing their way through programming problems making really long functions that are hard to debug and not really understanding what they're doing(not trying to diss them, they're very proficient in Mathematics, which I suck at). The boilerplate of class bodies, types, matching closing brackets, etc, make syntactic errors more prevalent as its easy to misspell keywords and close statements. Arguably even though error messages and modern IDEs give good hints, and that you gradually get used to knowing whats wrong, I notice that early on, crashes are taxing with frustration and disrupt "flow".

Overtly I think this is the best course for introduction to programming because it is more oriented towards teaching reasoning and intuition, stripping and reducing everything down to the minimum foundations of transforming data with just functions with good practices like reducing mutations and functional programming. Having to painstakingly type comments out on data definitions and write tests made me appreciate and understand static typing and features that I take for granted in other languages.

Not an endorsement but: https://youtu.be/1PhArSujR_A https://youtu.be/ydyztGZnbNs https://www.itworld.com/article/2978142/development/why-john-carmack-thinks-racket-is-aces-for-beginning-programmers.html John Carmack has a few good points on racket and functional programming.

Overtly I agree that some things are stupidly redundant but I think beginners appreciate the verbosity early on.

anon88391 commented 5 years ago

Personally I have found How To Code: Simple Data extremely dull and tedious which lead me to drop it and OSSU altogether since it's not possible to finish OSSU without completing the course. I think given the amount of complaints it's very important the course gets an alternative or even gets replaced.

xxylem commented 5 years ago

@anon88391

Firstly, the beauty of self-learning is that you have a great deal more freedom to decide your path. In this case, there are other courses you could work on first before coming back to this course:

Learning how to learn: It seems to be no longer listed on the curriculum but this is a very useful short course about learning in general and how to self-motivate/effective study methods.

In Systems: Introduction to Computer Science - CS50: An intro CS course from Harvard, but it doesn't go nearly as deep as How to Code. More lively though. Build a Modern Computer from First Principles: From Nand to Tetris: For part 1, you don't need any programming knowledge and it certainly was a very interesting insight into the low-level world of computer hardware.

In Maths; You could start on either linear algebra or calculus if you do not have these already.

When deciding on the next course, I open up a course, have a look and decide if it's the right one to do next. At times, I did two or three courses simultaneously, depending on the depth and complexity of the course. I would think about: is it too difficult or easy? is it the right next step: do I need this knowledge before anything else? Is there something else I need first? Am I interested in this?

Also, while video courses are more capitvating, I have found following textbooks to be easier sometimes. You can read much faster than people speak and obviously there can be much more and much more detailed written content than someone can record on video.

Regarding this specific course, I think long-term it was probably one of the most useful. In fact, the absence of bells and whistles means that it's all content. You learn systematic habits for building up data structures and performing operations on that data. Perhaps, after doing some of the other courses above and coming back to it, you will be properly motivated to go forth with it. If not, there is also the book that is was based on.

Anyway, if you have more questions, feel free to ask. Hope you manage to find a way through this blockage!

Josh

dropkickfish commented 5 years ago

Personally I have found How To Code: Simple Data extremely dull and tedious which lead me to drop it and OSSU altogether since it's not possible to finish OSSU without completing the course. I think given the amount of complaints it's very important the course gets an alternative or even gets replaced.

I'm pretty much just started in OSSU and have recently gone through the How To Code (or as much of them as I could whilst auditing and in a full time job before the time limit was up).

I appreciate that it can be dull. It can be frustrating if you've done a bit of other languages before. It can seem too simple or as though it's teaching you to suck eggs. It sounds like this is the emotional impact hanjiexi talked about.

However, even though I found these modules dull or repetitive, or even a bit simple at times after other more involved Python/Java classes, I still think that it's important for building a foundation and that can't be ignored. For those that don't know the concepts it teaches them. For those who have encountered them before it reinforces them. This is important for overall course structure.

At university I studied languages, and the first year was comprised of making sure all students understood the core concepts of the language that the rest of the degree was built on, regardless of who'd studied further at school or who was fluent in the language, or had it as their mother tongue. I don't see much difference here.

Because of that, I wouldn't consider dropping them from the course at all.

log101 commented 5 years ago

I'd like to point a different thing, which is more general, about design patterns. I'm pretty much frustrated about it right now. As (probably) you guys have seen, htdf(how to design function) and htdd(how to design data), are design patterns proposed for a good software engineering. As far as I've seen, there are even more of them outside the mooc. My question is, why is it considered to be right to apply a specific design pattern to individual problems. Yes, there are similarities between those problems but what about the parts that are problem-specific ? How to deal with these parts with a same set of patterns ?

jestarray commented 5 years ago

@log101 HTDF and HTDD is not really a design pattern, rather what I'd like to say, is the implicit cognitive thought process that everyone does in their heads when translating ideas to code, whether they are aware of it or not. If you write in a statically typed language, for the most part, you pretty much do everything in htdf and htdd except writing tests and explicitly writing comments about what variables and functions mean, relying on the names and intuition. I'm going against mindlessly and strictly following all steps of HTDF because as you program more and more, just like how you do math more and more, you will not need the extra verbosity, similar to how you used to write 9+9+9+9+9+9 to remember what the concept of 9 * 6 is. When you are off though, it is a good debugging technique because verbosity helps(https://en.wikipedia.org/wiki/Rubber_duck_debugging).

A design pattern is a set of code that solves specific problems (https://en.wikipedia.org/wiki/Software_design_pattern , strict description of when to use it) that keep reoccurring in programs, and people researched it and claim they have found that (x) set of code organized in (x) way is the best way to address the problem in a clean way.

https://youtu.be/JjDsP5n2kSM?t=755 A talk by Jonathan Blow sums this up pretty well. Data structures, design patterns, we all prematurely want to perfect and protect against problems that don't really exist yet. So when should you apply a design pattern? When problems actually start arising. Is your code being poorly understood? Is it becoming a pain to maintain? A lot of repetition and similarities everywhere? Then look into patterns and see if they apply to your specific problems.

https://youtu.be/jlcmxvQfzKQ another relevant video. In short, forget trying to apply too much of "the best structure" to something you have not even built yet. Make It Work, Make It Right, Make It Fast

log101 commented 5 years ago

@jestarray I think I get it... But I didn't get that part: "So when should you apply a design pattern? When problems actually start arising." But for example, how I am going to change the code I have been writing for a year so that it is OK according to a design pattern ? It wouldn't be hard ? Shouldn't we start with a design pattern from the beginning ?

jestarray commented 5 years ago

@log101 You will always be rewriting, scrapping and cleaning things up, and it shouldn't be too hard because it's just translating what you already have, to a more cleaner style.

Yes there will be some cases where you should read up on design patterns, like in game development, the design pattern of the game loop is a MUST that you really should start with from the beginning, but not all design patterns hit that critical mark.

I was in your position before, looking everywhere for the best way to do things before starting, reading a crap ton of books but it takes away from the joy of programming, and you will not remember all of them because that's not how your brain works. Imagine if someone said you have to read a 300 page strategy manual on the best way to play a game, before playing the game, because once you read it, you might do everything perfectly. I'm not saying you shouldn't look before you leap, but you shouldn't be too afraid of leaping. Besides, starting off with a sloppy implementation builds the meaning of why the design pattern is useful and will allow you to recognize when in other peoples code.

log101 commented 5 years ago

@jestarray Now I see, thanks for clearing thing up ! I should get to work then, until I feel a need for a pattern :d

waciumawanjohi commented 5 years ago

There is a current RFC open on replacing How to Code. Comments welcome! https://github.com/ossu/computer-science/issues/588