Help a beginner

77 views
Skip to first unread message

Arthur

unread,
Mar 2, 2023, 1:29:55 PM3/2/23
to 4tH-compiler
Hello,

I am currently trying to learn 4th and have been stuck while trying to do a simple plus or minus game.

game.4th
--------------------------

\ 4th - game - Basic plus or minus game
\ Arthur

include lib/choose.4th
include lib/enter.4th

: init
  ." Welcome to plus or minus game" cr
  99 choose 1 +
;

: ask
  begin
    dup
    ." What's your answer ?" cr
    enter over over
    >
    if ." It's more" cr
    else over over <
      if ." It's less" cr
      else ." You've won!" cr
      then
    then 
    \ stack is what I expect at this point : random number & input number
  until = \ stack contains two times the random number
;

init
ask

--------------------------
The problem is that 'until' always stop when I enter anything other than 0 and I do not understand why my stack changes when it tries to check the condition.

Any kind of help on that or the code in general would be welcome !

Arthur


The Beez

unread,
Mar 2, 2023, 4:24:41 PM3/2/23
to 4tH-compiler
Hi Arthur!

First of all, sorry for not responding sooner. I've overlooked your post. Your program is not hard to fix - just move the "=":

---8<---
\ 4th - game - Basic plus or minus game
\ Arthur

include lib/choose.4th
include lib/enter.4th

: init
  ." Welcome to plus or minus game" cr
  99 choose 1 +
;

: ask
  begin
    dup
    ." What's your answer ?" cr
    enter over over >
    if ." It's more" cr
    else over over <
      if ." It's less" cr
      else ." You've won!" cr
      then
    then =
  until
;

init
ask
---8<---

But I'm not done yet. Some improvements can be made. I'll show you later

Hans Bezemer

The Beez

unread,
Mar 3, 2023, 9:56:05 AM3/3/23
to 4tH-compiler
Let's make it a bit more compact. First - what is that "DUP" doing there? Where is the acute need to duplicate that one? Are more values put on top of that, so later only a "ROT" can be used to retrieve that value? No. Only one single value is put on top of that. So let's remove that "DUP".

After "ENTER" there are now *two* values on the stack - which we will compare later. But we can do much of that comparison by subtracting both values now - so only TWO values are left on the stack instead of THREE: "OVER -". So now we condense the three values accordingly:
  • If the subtraction renders LESS than zero, it's "less";
  • If the subtraction renders MORE than zero, it's more;
  • If the subtraction renders ZERO, we've won!
Now all these situations are mutually exclusive - so if speed isn't a problem, there is no need to nest them. As a matter of fact, we can make some simple words out of them:

: more? dup 0< if ." It's more" then ;
: less? dup 0> if ." It's less" then ;
: won?  dup 0= if ." You've won!" then ;

And we can reduce this large blob to: "more? less? won?". All texts contain a CR - so we might just do one of those after we've chosen. UNTIL will terminate the loop when the top of the stack (TOS) is non-zero, so we have invert that flag using "0=". Of course, when we exit the loop there is still the random number on the stack we have to eliminate by using DROP. Done. Now, your main loop looks like this:

: ask
  begin

    ." What's your answer ?" cr
    enter over - more? less? won? cr 0=
  until drop
;

Isn't that a whole lot easier to read? Of course, you still have this slight inefficiency of doing three comparisons per iteration. You can get rid of that one too, but it requires some more code. first of all, we need ">SIGN". It returns the sign of the number. If it's less than zero, it returns -1. If it's more than zero, it returns 1. It it's zero - it returns zero:

: >sign dup 0> swap 0< - ;             ( n -- -1|0|1)

You may try to figure this out, but it's a "you're not supposed to understand this one (yet)". BTW, in BASIC it's often implemented as the function SGN(). Now we can use it to compose our just-as-efficient decision tree using CASE..ENDCASE:

: winner?                              ( n -- f)
  dup case
   -1 of ." It's more" endof
    1 of ." It's less" endof
    ." You've won!"
  endcase cr 0=
;

Yes, we only got three cases to check: -1, 0 and 1. And if we've done two, we can forget about the other. All texts contain a CR - so we might just do one of those after we've chosen. Note that CASE..ENDCASE consumes our value, so we DUP it at the beginning. At the end we reverse the value using 0=, so a nice flag is returned for our UNTIL. So now we can reduce our ASK to this:

: ask
  begin

    ." What's your answer ?" cr
    enter over - >sign winner?
  until drop
;

So, ask a question, get an answer, get the sign of the subtraction and repeat this till you got a winner. That's easy to read, isn't it?

Ok, lessons learned:
  1. Keep your stack as shallow as possible;
  2. Get results as early as possible;
  3. Use short words you can easily test in isolation - especially conditionals and loops;
  4. Document your stack diagram;
  5. Don't repeat yourself if it's not absolutely and immediately required.
Hope this helps and keep me posted of your progress!

Hans Bezemer

p da

unread,
Jan 29, 2024, 5:05:30 PMJan 29
to 4tH-compiler
El viernes, 3 de marzo de 2023 a las 15:56:05 UTC+1, The Beez escribió:

can get rid of that one too, but it requires some more code. first of all, we need ">SIGN". It returns the sign of the number. If it's less than zero, it returns -1. If it's more than zero, it returns 1. It it's zero - it returns zero:

: >sign dup 0> swap 0< - ;             ( n -- -1|0|1)

At risk to be excesively pedant I'd like to note a little nuance about this code

the >sign word pretends to return 1 if positive, -1 if negative and 0 if zero, but it does the opposite, it return -1 for positive numbers and 1 for negative numbers, to work conform specifications it should be redefined as:

: >sign dup  0<  swap 0>  - ;   

it didn't affect to the great reply about how to code in forth, but just to be precise... 

regards
 
 

The Beez

unread,
Jan 29, 2024, 5:09:21 PMJan 29
to 4tH-compiler
You're mistaken. This is not Forth, this is 4tH.

Scr # 0
  0 : >sign dup 0> swap 0< - ;                                      
  1 -15 >sign . 16 >sign . 0 >sign .

-1 1 0

4tH's TRUE value is 1 - not -1.

Hans Bezemer

p da

unread,
Jan 29, 2024, 7:41:28 PMJan 29
to 4tH-compiler
El lunes, 29 de enero de 2024 a las 23:09:21 UTC+1, The Beez escribió:
You're mistaken. This is not Forth, this is 4tH.

4tH's TRUE value is 1 - not -1.

oh ok, my fault, I was wrongly assuming it works like Forth, thank you for pointing out my mistake


 

The Beez

unread,
Jan 30, 2024, 4:49:25 AMJan 30
to 4tH-compiler
There is an entire section in the manual discussing the differences between 4tH and Forth - and explaining WHY they are different:

Booleans
Another nice topic for a flame war is the value of truth. In ANS-Forth the 'TRUE' has the value "-1", which means all bits are set. Which is very clever. You can 'XOR', 'OR', 'AND' and 'INVERT' it with any other value and it will behave as logical value. But "the all bits set" flag has its drawbacks too. Let's see what the ANS-Forth standard says about flags:
"A FALSE flag is a single-cell datum with all bits unset, and a TRUE flag is a single-cell datum with all bits set. While Forth words which test flags accept any non-null bit pattern as true, there exists the concept of the well-formed flag. If an operation whose result is to be used as a flag may produce any bit-mask other than TRUE or FALSE, the recommended discipline is to convert the result to a well-formed flag by means of the Forth word 0<> so that the result of any subsequent logical operations on the flag will be predictable. In addition to the words which move, fetch and store single-cell items, the following words are valid for operations on one or more flag data residing on the data stack: AND OR XOR INVERT"
We highly recommend the discipline of converting a non-zero value to a well-formed flag. But we don't understand why 'INVERT' is a valid way to manipulate a flag. We'll try to explain you why.
Forth traditionally has no specific logical operators. Instead, binary operators were used. This put 'INVERT' (or 'NOT' as it was called in Forth-79) in a difficult position. 'INVERT'ing any non-zero value will result in a non-zero value, except when all bits are set.
That is why '0=' was introduced, a full-fledged logical operator. So why use 'INVERT' when you want to perform a logical operation? Another quote:
"Since a "char" can store small positive numbers and since the character data type is a sub-range of the unsigned integer data type, C! must store the n least-significant bits of a cell (8 <= n <= bits/cell). Given the enumeration of allowed number representations and their known encodings, "TRUE xx C! xx C@" must leave a stack item with some number of bits set, which will thus will be accepted as non-zero by IF."
This is another problem of using "all bits set" as a true flag: you store a well formed flag in an address unit that should easily be able to handle it and you'll never get it back. A flag is a boolean and can have two values: either true or false. The smallest unit that can hold a boolean is a bit. ANS-Forth programmers are denied that privilege.
But why are some Forth programmers so keen on their “all bits set” flag? Well, you can do neat things with it.
: >CHAR DUP 9 > 7 AND + ASCII 0 + ;
This will convert a digit to its ASCII representation. True, it is a clever piece of programming, but in our opinion it is bad style. Why? Because you are using a flag as a bitmask, which is a completely different datatype. Although there is no such thing as “data typing” in Forth, this way of programming makes it difficult to understand and maintain a program, which the ANS-Forth standard acknowledges:
"The discipline of circumscribing meaning which a program may assign to various combinations of bit patterns is sometimes called data typing. Many computer languages impose explicit data typing and have compilers that prevent ill-defined operations. Forth rarely explicitly imposes data-type restrictions. Still, data types implicitly do exist, and discipline is required, particularly if portability of programs is a goal. In Forth, it is incumbent upon the programmer (rather than the compiler) to determine that data are accurately typed."
But there is an even more compelling reason why 4tH returns ”1” as the 'TRUE' value. ”-1” is only valid in a 2-complement context. What it actually means is ”all bits set”. Mathematically it means nothing, contrary to ”1”, which is return value associated with the Iverson bracket (https://en.wikipedia.org/wiki/Iverson_bracket).
.
That is why 4tH uses "1" as a true flag. Usually, it won't make much difference. Except when you use 'INVERT' to invert a flag or intend to make obfuscated programs. If you use '0=' instead, you won't run in any trouble, not even when you port your program to ANS-Forth. Clarity may introduce a little overhead, but in this age of multi-gigaherz machines, who is counting? E.g. you could program “>CHAR” like this:
                                          \ convert a flag to a bit mask 
: >MASK 0 SWAP IF INVERT THEN ;           ( f -- mask)
                                          \ convert a digit to ASCII
: >CHAR DUP 9 > >MASK 7 AND + ASCII 0 + ; ( n -- c)
If you still want to change the true flag, you can by simply changing a #define in cmds_4th.h:
#define F_T ~(0L)
But we doubt whether it will be a great benefit to your programming style.



Reply all
Reply to author
Forward
0 new messages