~icefox

Just foxing about.

https://wiki.alopex.li/

Trackers

~icefox/garnet

Last active a day ago

~icefox/oorandom

Last active 21 days ago

~icefox/scalar

Last active 1 year, 4 months ago

~icefox/cf_issues

Last active 2 years ago

#33 Thoughts on macros a day ago

T-THOUGHTS added by ~icefox on ~icefox/garnet

#33 Thoughts on macros a day ago

Ticket created by ~icefox on ~icefox/garnet

I've been quietly assuming all along that there will be SOME kind of macro system, though admittedly compile-time metaprogramming may just do the exact same job.

Useful conversation from #fennel:

11:07 < seabass_> by the way technomancy, your example in https://git.sr.ht/~technomancy/fennel/tree/main/item/macros.md#identifiers-and-gensym doesn't account for Klingon spies

11:07 < seabass_> definitely a problem on production starships

11:21 < ridcully_> but it works for when they are on the starboard bow

11:27 < technomancy[m]> lol

11:44 < Icefoz> Is that an example of "hygenic macros"? That's one of those terms I've always thought I understood but was never quite sure.

11:45 < Icefoz> That's actually a pretty nice way of making sure new identifiers in macros don't clash...

11:46 < technomancy> it's not hygenic but it's related

11:47 < technomancy> a hygenic system means it's impossible to write a macro that has a collision

11:47 < technomancy> whereas fennel's macro system makes it so it's impossible to have a collision by accident which solves serious problems in CL's macro system

11:49 < Icefoz> Aha. So with a hygenic system if you want to use an identifier from outside the macro, it has to be passed in?

11:55 < technomancy> more or less yeah. it's a big improvement over CL's macro system, but IMO it's a bit of an overreaction; you can get the same benefit with less restrictions

11:58 < Icefoz> Hmmmm, nice.

11:59 < Icefoz> Now I have to fit this into my head around what I know of Rust's macro system, which has weird identifier rules of its own.

12:23 < ridcully_> first rule of macro-club then: don't put ! at the end

12:31 < Icefoz> but I like shouting. ;_; When I make my own lang its macros are gonna have to be all in caps.

#32 Memory allocator references 11 days ago

T-LATER added by ~icefox on ~icefox/garnet

#32 Memory allocator references 11 days ago

Ticket created by ~icefox on ~icefox/garnet

Eventually we'll probably be writing or using an existing memory allocator. Info on options:

#7 Wrong default incrementor value? 21 days ago

Bug added by ~icefox on ~icefox/oorandom

#7 Wrong default incrementor value? 21 days ago

Ticket created by ~icefox on ~icefox/oorandom

zuurr — Yesterday at 9:24 PM i have a bug report for oorandom. you don't use github or i'd just file it as a pr (i dont really want to make an account on your thing to file an issue, so i'm just messaging it to you) you have the wrong default for the incrementor. e.g. you take the value from the PCG source, but then do inc.wrapping_shl(1) | 1 to it this causes your result to differ from the reference impl basically you just need to change https://docs.rs/oorandom/11.1.3/src/oorandom/lib.rs.html#39 to be

pub const DEFAULT_INC: u64 = 1442695040888963407 >> 1;

or something

same thing for https://docs.rs/oorandom/11.1.3/src/oorandom/lib.rs.html#170

let me know if that doesn't make sense. i'm 100% positive this is a bug, and can probably do a better job explaining than i just did. well, it's a bug if you care at all about having the same results as the reference implementation. which IMO you should?

#16 Create rationale document 24 days ago

Comment by ~icefox on ~icefox/garnet

#8 Thoughts on modules 25 days ago

Comment by ~icefox on ~icefox/garnet

Notes from reading this book about SML. (Also it turns out this book is not the best, I like this one more.)

Being able to import a structure/module's names into the local scope is useful, consider the Rust:

enum Foo {
  A,
  B
}

use Foo::*;
match something {
  A => ...,
  B => ...,
}

Enums should define a structure that is their contents. At least C-like enums should:

enum Foo =
  A,
  B,
end

-- Is much like:

struct FooThing =
  A: Foo,
  B: Foo,
end

const Foo: FooThing = Foo {
  A = magic,
  B = magic,
}

So calling Foo.A is literally just a struct lookup and works exactly like it. This means that this struct can also have an iter() function that iterates over all its possible members.

This works even with non-C-like enums, which I will call sum types for now for lack of anything better. In this case if the value is a type constructor of some kind, then it's just exposed as a function. Inevitably though, in this case it doesn't seem like we can have an iter() function on the struct since the things it returns won't necessarily all be the same type. I mean... they'll all be type Foo, but some of them will be normal-ass values but others will be functions. This is fine, just a little squirrelly compared to the C-like case where the contents are very much just homogeneous numbers -- it's almost more like an array than a struct.

Okay, SML modules, like its (and OCaml's) structures, are "open". If you have a module signature containing members a, b, c and you have a structure containing members a, b, c, d then that module matches that signature with no extra definition, I think. This is also a little like Go's protocols. It has some (marginally) complex subtyping rules for making this work out; I think I will prefer to take the Rust trait approach and make everything explicit. However, it may be worth considering changing this decision if it significantly reduces boilerplate without hurting clarity too much.

Let's briefly consider what it would look like without explicit implementations. Everything would become basically bind-by-name, which I don't particularly like. Implementations would be implicit. On the other hand, you could define a trait/signature after the fact to match an existing structure, and I could see that it could make trait coherence not a problem because types only have one implementation of a particular signature, it just may match multiple signatures at a time and it depends entirely on what the receiver expects to get. Onnnnnnn the flip side, no idea how to implement that well. We will see.

Trait coherence is an interesting problem to solve. It can be phrased as "you can't have two implementations of a trait for a type", and the way it solves it is "you can only implement traits you define on types you don't define, or any traits on types you do define." This is actually quite cunning, but I am not really sure why you can't just choose explicitly which trait implementation to use out of multiple possibilities.

Now... SML's module signatures support subtyping. So if you have

signature FOO =
  sig
    val thing: 'a -> 'a
  end

signature BAR =
  sig
    val thing: int -> int
  end

then FOO matches the signature BAR because if you have a struct implementing FOO then you can stuff an int into it and its signature will be the same as BAR's. Hmmmm, ok.

Also it considers functions and enum type constructors the same, so these match:

signature RBT DT =
  sig
    datatype ’a rbt =
      Empty |
      Red of ’a rbt * ’a * ’a rbt |
      Black of ’a rbt * ’a * ’a rbt
  end

signature RBT =
  sig
    type ’a rbt
    val Empty : ’a rbt
    val Red : ’a rbt * ’a * ’a rbt -> ’a rbt
  end

There's some kind of "type ascription" functionality involved with ensuring a structure implements a module or something, and my eyes are really starting to glaze over. I might need a more interesting book. In a lot of ways this junk is more complex thatn the Rust trait equivalents, 'cause they feel the need to special case lots of little things. Type ascription is basically "are these types transparent or abstract", corresponding to newtype structs and public fields in Rust, and it's just like... not necessary. Ok, that's fine.

Now, let's consider a simple binary tree dictionary:

enum Order =
  Lt,
  Eq,
  Gt,
end

struct Ord =
  T: type,
  cmp: fn(T, T) -> Order,
end

struct TreeDict =
  K: type Ord,
  V: type,
  Dict: type,

  new: fn(): Dict,
  insert: fn(Dict, K, V): Dict,
  lookup: fn(Dict, K): Option[V],
end 

-- Implementation...
const IntOrd = Ord {
  T: i32,
  cmp: fn(T, T) -> Order = ... end
}

sum TreeDictType[Key,Val] =
  Empty,
  Node{Key, Val, TreeDictImpl[Key, Val], TreeDictImpl[Key, Val]}, 
end

const TreeDictImpl[Key, Val] = TreeDict {
  K: type Key,
  V: type Val,
  Dict: TreeDictType[K, V],

  new: fn(): Dict = Dict.Empty end
  insert: fn(Dict, K, V): Dict = ... end
  lookup: fn(Dict, K): Option[V] = ... end
}

let thing1: TreeDictType[IntOrd.T, ()] = TreeDictType.Empty
let thing2: TreeDictImpl[IntORd.T, ()] = TreeDictImpl.new()

Hmmmmm. Doesn't look like the worst thing ever. The IntOrd thing is a little squirrelly still. SML slices things in a slightly different way, I'd basically have to do:

signature STRING_DICT =
  DICT where type Key.t=string
signature INT_DICT =
  DICT where type Key.t=int

Then there's type sharing constraints that are kinda necessary in some ways, but in other ways may just be a consequence of poor design of data types, sooooo it's a little weird. Struct definitions don't have generic types attached to them, you can't do 'a DICT where 'a=string or something like that, quite.

What is at issue here is a fundamental tension in the very notion of
modular programming. On the one hand we wish to separate modules
from one another so that they may be treated independently. This re-
quires that the signatures of these modules be self-contained. Unbound
references to a structure — such as Point — ties that signature to a spe-
cific implementation, in violation of our desire to treat modules separately
from one another. On the other hand we wish to combine modules together
to form programs. Doing so requires that the composition be coherent,
which is achieved by the use of sharing specifications. What sharing spec-
ifications do for you is to provide an after the fact means of tying together
several different abstractions to form a coherent whole.

And then:

To support code re-use it is useful to define generic, or parameterized, mod-
ules that leave unspecified some aspects of the implementation of a mod-
ule. The unspecified parts may be instantiated to determine specific in-
stances of the module. The common part is thereby implemented once and
shared among all instances.

In ML such generic modules are called functors. A functor is a module-
level function that takes a structure as argument and yields a structure
as result. Instances are created by applying the functor to an argument
specifying the interpretation of the parameters.

Ah, so we CAN do the parameterized dictionariy definitions as above, those just get called functors. Functions that take a structure and return a new structure. That's what's necessary to get the Rust-like TreeMap<K,V> behavior I want.

So, read and try to implement chapter 23 again

#23 Thoughts on teh fastest evar error handling a month ago

Comment by ~icefox on ~icefox/garnet

Ooh, I hadn't seen that. Thank you! Looks like a useful resource. I don't know if I'm ever going to actually implement anything fancier than Rust's Result types, but it may just become easy to do something more efficient at some point. It just occurred to me that the compiler has all the info it needs to treat result types as a special case, so they can be semantically the same but the implementation could be special cased.

#16 Create rationale document a month ago

Comment by ~icefox on ~icefox/garnet

I really kinda want Garnet to be the Lua of low level languages.