untoldwind / KontrolSystem2

Autopilot scripting system for KSP2
Other
54 stars 14 forks source link

Request for Array method #114

Closed lefouvert closed 1 day ago

lefouvert commented 5 months ago

Hi, it would be wonderfull if Array type implement those two methods :

(or OrderBy and Aggregate as said in .net framework)

And it would be nice we could edit collection, with some kind of .Add .Remove.

Kindly, LeF.

Edit : typo

untoldwind commented 5 months ago

The sort is easy. With the reduce I encountered some issues with the type-inference: Something like this works:

    let int_arr = [ 17, 11, 3, 7, 5, 13 ]

    let sum = int_arr.reduce(0, fn(sum : int, item: int) -> sum + item)

but when removing the type-hints from the inner function (i.e. just int_arr.reduce(0, fn(sum, item) -> sum + item)) the compiler is currently not able to deduce the return type. There is something of a hen-egg problem with the current implementation.

As for the add/remove: I was very careful to keep the array immutable (mostly because I had some very bad experiences with javascript in this regard :face_exhaling: ). The array does support the + and += operators though.

So something like this works:

let int_arr : int[] = []
for(i in 0...4) {
  int_arr += i
  int_arr = int_arr + i // same as above, note: int_arr + i creates a new array
}

I also added a slice method, so that removal of an element could be done like this:

int_arr.slice(0, to_remove) + int_arr.slice(to_remove + 1)

If you really need a mutable array/list/queue I would rather like to add specific type for that

lefouvert commented 5 months ago

If you really need a mutable array/list/queue I would rather like to add specific type for that

As I come from old fashionned procedurals languages (C, ASM 68k, Cobol), even if I have fun with more recent approach, I still struggle to apprehend all subtilities of those.

I think you are right, an array should be unmutable.

Since it was the only collection, I was thinking about it only as collection, but if it's still in language phylosophy, an other kind of collection, as a list, or a set could be usefull, and it would preserve the unmutability of the array. (Unless unmutability is expected for all kind of collection ^^)

Keep in mind that although I knew the existence of functionnal language, Kontrol is my first time encounter - I kinda like it :) - , so my request could be irrelevant : maybe it's on me to learn to work differently.

+= slice

Thanks for the tips !

With the reduce I encountered some issues with the type-inference

When I first used a reduce, that's exactly what I asked myself: "How the hell do they know which type goes where?" ". I'm kind of glad I'm not alone.

The sort is easy

As expected :)

very bad experiences with javascript

Imagine a black cat, all bristly fur, hissing while threatening with his claws. It's me when I code in javascript. The only times I approach this 'thing' is when I only have a highly restricted authority computer which don't allow me to install any sdk. I feel your misgivings.

untoldwind commented 5 months ago

This should now be part of 0.5.2.4

    const sum = int_arr.reduce(0, fn(sum, item) -> sum + item)

also works, though the type-inference certainly needs more testing

lefouvert commented 5 months ago

version 0.5.2.4 (Ckan)

Need a dumb monkey doing dumb things to nice tools to see if they are dumbproof? Here I am!

gitreport::reduce.to2

use { Vessel } from ksp::vessel

fn sumint() -> int = {
    const arr_int = [1, 2, 3, 4, 5, 6]
    const sum = arr_int.reduce(0, fn(sum, item) -> sum + item)
    return sum
}
// Rebooted in 00:00:02.9630349
// No errors

// Doc refer to vessel.part as an array : 'parts | ksp::vessel::Part[] | R/O | Get a list of all vessel parts.'
fn fuel_flow(vessel: Vessel) -> float = {
    return vessel.parts
        .filter(fn(p) -> p.engine_module.defined)
        .map(fn(p) -> p.engine_module.value.max_fuel_flow)
        .reduce(0.0, fn(sum, f) -> sum += f)
}
// Rebooted in 00:00:02.5323374
// ERROR: [gitreport\reduce.to2(17, 36)] NoSuchVariable
// Local variable 'sum' is read-only (const)

// Also broke, but less intriging since I use a Range
fn factorial(n: int) -> int = {
    return (1...n)
        .reduce(1, fn(f, x) -> f *= x)
}
// Rebooted in 00:00:02.6255638
// ERROR: [gitreport\reduce.to2(27, 18)] NoSuchMethod
// Type 'Range' does not have a method or field 'reduce'
// ERROR: [gitreport\reduce.to2(26, 1)] IncompatibleTypes
// Function 'factorial' returns Unit but should return int

// Also broke, even if I trick the compiler converting the Range into an Array
fn factorial(n: int) -> int = {
    return (1...n)
        .map(fn(x) -> x)
        .reduce(1, fn(f, x) -> f *= x)
}
// Rebooted in 00:00:02.6332422
// ERROR: [gitreport\reduce.to2(42, 32)] NoSuchVariable
// Local variable 'f' is read-only (const)

Edit : misspelling in comments Edit : code readability

untoldwind commented 5 months ago

The reduce method for ranges will be added with the next patch. The Local variable 'f' is read-only error though is kind of by design, i.e. all function parameters are treated as const.

In your example: If you just replace fn(f, x) -> f *= x with fn(f, x) -> f * x it should work.

After all reduce is not a loop: [item0, item1, item2].reduce(initial, reducer) is supposed to be equivalent to: reducer(reducer(reducer(initial, item0), item1), item2)))

lefouvert commented 5 months ago

Han..! I now see where I diverge from your exemple. Maybe it's tiredness that's crumbled me. I'll take a nap before have fun with those .reduce :)

Thank you !

lefouvert commented 5 months ago

Hello again :) I've just seen that .sort doesn't take any parameters, so I assume sorting only works on arrays whose type is compatible with standard comparison operators. Is it possible to do this with an (optional?) parameter that defines a comparison subject for complex structures (like a Record, for example, or, in my test case, the activation_stage of a part :))

untoldwind commented 5 months ago

I had some problems with an optional function parameter, so I added .sort_by and .sort_with instead.

Example:

pub struct TestStruct(i: int) {
    field1: int = i
}

const struct_arr : TestStruct[] = [
        TestStruct(1),
        TestStruct(-6),
        TestStruct(2),
        TestStruct(10),
        TestStruct(-4),
    ]

const sort_asc = struct.arr.sort_by(fn(t) -> t.field1) const sort_desc = struct_arr.sort_by(fn(a, b) -> if(a.field1 > b.field1) -1 else if(a.field1 == a.field2) 0 else 1)


I.e. `.sort_by` is mapping the element to something that is sortable (like int/string) and `.sort_with` is using an explicit comparator.
lefouvert commented 5 months ago

version 0.5.2.5 (Ckan)

sort_* are perfect. Thank you.


Since it's also about Arrays, I have a question. In the doc, I could read about tuples

use { floor } from core::math
fn floor_remain(a : float) -> (int, float) = (floor(a).to_int, a - floor(a))

In assignments tuples can be deconstructed:

 let (b, c) = floor_remain(12.34)

will define an integer variable b with value 12 and a float variable c with value 0.34

I feel (maybe wrongly) than .filter and co. make an assigment in the fn(something) -> declaration. So I tried this : gitreport::arrtupledeconstruct.to2


fn foo() -> (int, string)[] = {
    const foo = [(0, "zero"), (1, "one"), (2, "two"), (3, "three")]
    return foo
        .filter(fn((number, letter)) -> number == 2)
}

fn bar() -> (number: int, letter: string)[] = {
    const bar = [(number: 0, letter: "zero"), (number: 1, letter: "one"), (number: 2, letter: "two"), (number: 3, letter: "three")]
    return bar
        .filter(fn(rec) -> rec.number == 2)
}

and I get this error from foo function:

Rebooted in 00:00:02.4392074 ERROR: [gitreport\arrtupledeconstruct.to2(5, 16)] Parsing gitreport\arrtupledeconstruct.to2(5, 16): Expected \

Is that expected ?

Anyway, as you can see, bar function, even if it doesn't deconstruct the record, work perfectly and is a satisfying workaround. It's just to u know, if it wasn't expected.

Edit : typo

untoldwind commented 5 months ago

Sorry, overlooked this comment. So far I have not implement deconstructing in function/lambda parameters yet, so the offending part is: fn((number, letter)) -> number == 2

Instead you could do it like this:

    return foo
        .filter(fn(tuple) -> tuple._1 == 2)
lefouvert commented 5 months ago

It happen. And I'm maybe a bit talkative, too ^^ I had uderstood the unimplmented deconstructing in fn/lambda, but i'm happy to learn than tuple items can be accessed this way !

github-actions[bot] commented 3 months ago

This issue is stale because it has been open for 60 days with no activity.

lefouvert commented 2 months ago

Hi !

It seems VSC extention have some trouble to understand the new property array.is_empty for unusual cases :

sync fn group_statics_as_rotaries(staticSolarPanels: Part[]) -> Part[][] = {
    if(staticSolarPanels.length < 2)
        return []

    (0..staticSolarPanels.length)
        .flat_map(fn(i) -> ((i + 1)..staticSolarPanels.length).map(fn(j) -> normal_for_partpair(staticSolarPanels[i], staticSolarPanels[j])))
        .reduce(<(parts: Part[], normalId: GlobalVector)>[], group_by_normal)
        .map(fn(r) -> r.parts)
        .filter(exclude_asymetric_enlightenment)
        .sort_by(fn(a) -> a.length)
        .reverse()
        // .reduce(<Part[]>[], deduplicate) // TODO
        .reduce(<Part[]>[], fn(clean, partArr) -> {
            if(clean.is_empty)
                return clean + partArr
            return clean + partArr.filter(fn(part) -> !clean.flat_map(fn(a) -> a).exists(fn(p) -> p.position == part.position)) // TODO prefer a part.id than a part.position when it will be available
        }) // deduplicate
        .filter(fn(a) -> a.length > Array.Empty)
}

leads to

Undefined field is_empty for type Part[][]

on if(clean.is_empty), for example.

Note : Same struggles happen with Records array as seen on this alert (or is it because it's somewhat matricial with an array in an other ?):

Undefined field is_empty for type (parts : Part[], normalId : GlobalVector)[]

And since I find .is_empty a really nice adding (You have seen my Array.Empty, didn't you ? ^^), could string benefit from the same features ? Cherry on the cake would be have a empty property on string which could allow us to use some string.empty as equivalent of "". (should be linked with issue #123)

lefouvert commented 2 months ago

Still on arrays, but other anomaly, about += operator : I have this :

pub struct Logger() {
    blackboxes: (vesselUID: string, entries: LogEntry[])[] = []
}

impl Logger {
// some other methods...
    fn new_entry(self, entry: LogEntry) -> Unit = {
        self.blackboxes = self.blackboxes + (vesselUID: entry.vessel.id, entries: [entry])
    }
}

Everything goes fine.

BUT ! If I dare to write

    fn new_entry(self, entry: LogEntry) -> Unit = {
        self.blackboxes += (vesselUID: entry.vessel.id, entries: [entry])
    }

I got this error :

Rebooted in 00:00:03.7620438 ERROR: [ui\logger.to2(53, 9)] IncompatibleTypes Type 'Logger' field 'blackboxes' is of type (entries : ui::logger::LogEntry[], vesselUID : string)[] but is assigned to (entries : LogEntry[], vesselUID : string)

untoldwind commented 2 months ago

Arrays are somewhat special and mostly hardcoded ein the vscode extension, I just forgot to add the is_empty there. This version should fix it: https://github.com/untoldwind/KontrolSystem2/releases/download/v0.5.8.3/to2-syntax-0.0.50.vsix

The += is an issue on the field assignment (it works for variable assignments). Should be fixed in the next release

untoldwind commented 2 months ago

Should work in 0.5.8.4

github-actions[bot] commented 2 weeks ago

This issue is stale because it has been open for 60 days with no activity.

github-actions[bot] commented 1 day ago

This issue was closed because it has been inactive for 14 days since being marked as stale.