“ blub悖论”和c ++


37

我在这里阅读文章:http : //www.paulgraham.com/avg.html,有关“ blub悖论”的部分特别有趣。作为主要使用c ++编写代码但接触过其他语言(主要是Haskell)的人,我知道这些语言中的一些有用的东西很难在c ++中复制。问题主要是针对精通c ++和其他某种语言的人,您是否使用某种强大的语言功能或惯用语言,如果仅使用c ++编写则很难概念化或实现?

特别是这句话引起了我的注意:

通过归纳法,唯一能够看到各种语言之间功能差异的程序员就是那些理解能力最强的程序员。(这可能是Eric Raymond所说的Lisp,使您成为更好的程序员的意思。)由于Blub悖论,您不能相信其他人的意见:他们对碰巧使用的任何语言都感到满意,因为它决定了语言的正确性。他们对程序的思考方式。

如果事实证明我等同于使用C ++的“ Blub”程序员,那么就会引发以下问题:如果您遇到过其他语言所遇到的有用概念或技术,您会发现很难概念化在用C ++编写或“思考”?

例如,可以使用Castor库在c ++中实现在Prolog和Mercury等语言中看到的逻辑编程范例,但最终我发现从概念上讲,我在考虑Prolog代码,并在使用时转换为c ++等效语言。为了扩大我的编程知识,我试图找出是否存在其他类似的有用/强大习语示例,它们可以用其他语言更有效地表达,而我可能不是C ++开发人员。我想到的另一个例子是lisp的宏系统,从程序内部生成程序代码似乎对某些问题有很多好处。这似乎很难在c ++中实现和考虑。

此问题并非旨在进行“ C ++ vs Lisp”辩论或任何形式的语言战争类型辩论。提出这样的问题是我发现有可能找出我不知道的事情的唯一途径。



2
我同意。只要这不会成为C ++与Lisp的争论,我认为这里有一些要学习的东西。
jeffythedragonslayer

@MasonWheeler:there are things that other languages can do that Lisp can't-不太可能,因为Lisp是图灵完备的。也许您是想说Lisp 中有些事不切实际?我可以对任何编程语言说同样的话。
罗伯特·哈维

2
@RobertHarvey:“在等同于图灵的意义上,所有语言都具有同样强大的功能,但这并不是程序员所关心的词。(没人想对图灵机进行编程。)程序员所关心的那种能力可能不是可以正式定义,但一种解释方式是说它是指您只能通过为功能更强大的语言编写解释器而获得功能更强大的语言的功能。” -保罗·格雷厄姆(Paul Graham),在有关主题的脚注中。(明白我的意思吗?)
梅森·惠勒

@梅森·惠勒:(不是真的。)
罗伯特·哈维

Answers:


16

好吧,既然您提到了Haskell:

  1. 模式匹配。我发现模式匹配更容易读写。考虑一下map的定义,并考虑在没有模式匹配的情况下如何用一种语言实现它。

    map :: (a -> b) -> [a] -> [b]
    map f [] = []
    map f (x:xs) = f x : map f xs
    
  2. 类型系统。有时可能会很痛苦,但是非常有帮助。您必须对其进行编程才能真正理解它以及它捕获了多少错误。同样,引用透明性也很棒。在Haskell中编程一段时间后,只有通过命令式语言管理状态才引起多少错误,这才变得显而易见。

  3. 一般的函数式编程。使用地图和折页而不是迭代。递归。这是关于更高层次的思考。

  4. 懒惰的评估。同样,它是关于更高层次的思考并让系统处理评估。

  5. 阴谋,包和模块。对于我来说,有了Cabal下载软件包比查找源代码,编写makefile等要方便得多。仅导入某些名称比本质上将所有源文件一起转储然后编译起来要好得多。


2
关于模式匹配的注释:我一般不会说它更容易编写,但是在您对表达式问题有所了解之后,很显然,诸如if和switch语句,枚举和观察者模式之类的东西都是代数数据类型的劣等实现。 +模式匹配。(并且甚至不让我们开始也许如何使空指针异常过时)
hugomg

您说的是正确的,但是表达问题是关于代数数据类型的限制(以及标准OOP的双重限制)。
Blaisorblade

@hugomg您的意思是访问者模式而不是观察者模式吗?
塞巴斯蒂安·雷德尔

是。我总是切换这两个名称:)
hugomg '16

@hugomg并不是要拥有Maybe(对于C ++请参见std::optional),而是必须将事物显式标记为可选/可为空/也许。
重复数据删除器

7

记住!

尝试用C ++编写它。不适用于C ++ 0x。

太麻烦了吗?好的,尝试使用 C ++ 0x。

看看您是否可以在D中击败此4行(或5行,无论:P)的编译时版本:

auto memoize(alias Fn, T...)(T args) {
    auto key = tuple(args);                               //Key is all the args
    static typeof(Fn(args))[typeof(key)] cache;           //Hashtable!
    return key in cache ? cache[key] : (cache[key] = Fn(args));
}

调用它所需要做的就是:

int fib(int n) { return n > 1 ? memoize!(fib)(n - 1) + memoize!(fib)(n - 2) : 1;}
fib(60);

您还可以在Scheme中尝试类似的方法,尽管它会慢一些,因为它在运行时发生,并且因为这里的查询是线性的而不是哈希的(而且,因为它是Scheme):

(define (memoize f)
    (let ((table (list)))
        (lambda args
            (cdr
                (or (assoc args table)
                    (let ((entry (cons args (apply f args))))
                        (set! table (cons entry table))
                        entry))))))
(define (fib n)
        (if (<= n 1)
            1
            (+ (fib (1- n))
                (fib (- n 2)))))))
(set! fib (memoize fib))

1
因此,您喜欢APL,您可以在其中单行编写任何内容?大小无所谓!
Bo Persson

@Bo:我还没有使用过APL。我不确定“大小无关紧要”是什么意思,但是我的代码有什么问题让您这么说吗?在用我不知道的另一种语言(例如C ++)执行此操作时,是否有一些优势?(如果您要使用的话,我会稍微编辑一下变量名。)
Mehrdad

1
@Mehrdad-我的评论是关于最紧凑的程序并不是最好的编程语言的标志。在那种情况下,APL会很轻松,因为您只需使用一个char运算符就可以完成大多数事情。唯一的问题是它不可读。
Bo Persson

@Bo:就像我说的那样,我不推荐APL。我什至从未见过。大小是一个标准(尽管很重要,如果您使用C ++可以看到)……但是代码有什么问题吗?
Mehrdad

1
@Matt:您的代码已记忆一个功能,但是此代码可以记忆任何功能。这些并不是完全等效的。如果您实际上尝试在C ++ 0x中编写这样的高阶函数,则它比D中的代码要乏味得多(尽管它仍然很有可能……尽管在C ++ 03中是不可能的)。
Mehrdad

5

C ++是一种多范式语言,这意味着它试图支持多种思维方式。有时C ++功能比另一种语言的实现更尴尬或不太流畅,例如函数式编程。

就是说,我想不出yieldPython或JavaScript 所具有的本机C ++语言功能。

另一个例子是并发编程。C ++ 0x对此有发言权,但当前标准没有,并且并发是一种全新的思维方式。

而且,即使您从未离开C ++编程领域,也永远不会学习快速开发-甚至Shell编程。


我什至无法开始考虑在给定C ++ 2003的情况下用C ++创建生成器会有多么困难。C ++ 2011将使其变得更容易,但仍然是不平凡的。常规使用C ++,C#和Python,生成器很容易成为我在C ++中最想念的功能(现在C ++ 2011已添加了lambda)。

我知道我会为此而努力,但是如果我绝对必须使用 C ++实现生成器,则必须使用... setjmplongjmp。我不知道该中断多少,但我想例外会是第一个。现在,如果您能原谅我,我需要重新阅读Modern C ++ Design,以使我摆脱困境。
Mike DeSimone

@Mike DeSimone,您能否(简要地)详细说明如何尝试使用setjmp和longjmp解决方案?

1
协程对函子是同构的。
GManNickG

1
@ Xeo,@ Mike:xkcd.com/292
Mehrdad

5

协程是一种非常有用的语言功能,比起C ++,其他语言还具有许多更明显的好处。它们基本上提供了额外的堆栈,因此功能可以被中断和继续,为语言提供了类似于管道的功能,可以轻松地通过过滤器将操作结果提供给其他操作。很棒,在Ruby中,我发现它非常直观和优雅。懒惰的评估也与此相关。

内省和运行时代码的编译/执行/评估/是C ++所缺乏的强大功能。


协程可在FreeRTOS中使用(请参阅此处),该程序已在C中实现。我想知道如何使它们在C ++中工作?
Mike DeSimone

协同例程是在C语言中模拟对象的讨厌工具。在C ++中,对象用于捆绑代码和数据。但是在C语言中,您不能。因此,您可以使用协同例程堆栈来存储数据,并使用协同例程功能来保存代码。
MSalters 2011年


1
@Ferruccio:感谢您提供的链接... Wikipedia文章中也有一些内容。@MSalters:是什么使您将协同例程描述为“讨厌的黑客”?对我来说似乎是一个非常武断的观点。使用堆栈来存储状态也可以通过递归算法来完成-它们也很黑吗?FWIW,协程和OOP大约是在同一时间(1960年代初期)出现的。。。说前者是C语言对象的黑客,这似乎很奇怪。在C ++之前> 15年。
托尼(Tony)

4

在Lisp和C ++中都实现了计算机代数系统后,我可以告诉您,即使我是该语言的新手,但在Lisp中该任务要容易得多。列出的所有内容的这种简单性简化了许多算法。诚然,C ++版本快了数十亿倍。是的,我本可以使Lisp版本的速度更快,但是代码不会那么简单。例如,脚本编写是另一件事,它总是会变得更加容易。所有这些都是使用正确的工具完成工作。


速度有什么区别?
quant_dev

1
@quant_dev:当然是千亿的倍数!
马特·艾伦

我从来没有真正测量过它,但是我感觉到大O是不同的。我最初以功能样式编写C ++版本,并且在我教它修改数据结构而不是创建新的,经过更改的结构之前,它还存在速度问题。但这也使代码更难阅读...
jeffythedragonslayer 2011年

2

当我们说一种语言比另一种语言“更强大”时,我们是什么意思?当我们说一种语言是“表达性的”时?还是“有钱?” 我认为我们的意思是,当一种语言的视野缩小到足以使描述问题变得轻松自然时,它便会获得力量-真的是状态转换,不是吗?-处于那种观点之内。但是,当我们扩大视野时,这种语言的功能,表现力和实用性将大大降低。

语言越“强大”和“富于表现力”,其使用就越受限。因此,“强大”和“富有表现力”可能是错误的词汇,用于狭义实用程序的工具。对于此类事情,也许“适当”或“抽象”是更好的说法。

我从编程开始,首先编写了许多低级的东西:带有中断例程的设备驱动程序;嵌入式程序;操作系统代码。该代码与硬件紧密相关,我都是用汇编语言编写的。我们不会说汇编程序不是最抽象的,但是它曾经是并且也是所有语言中最强大和最富表现力的语言。我可以用汇编语言表达任何问题;它是如此强大,我可以用任何机器做任何我想做的事情。

我后来对高级语言的所有理解都归功于我在汇编器方面的经验。后来我学到的一切都很容易,因为,您看到,一切(无论多么抽象)最终都必须适应硬件。

您可能想忘记越来越高的抽象水平,即越来越狭窄的视野。您以后总是可以接听。几天之内就可以轻松学习。在我看来,最好学习硬件1的语言,以尽可能接近骨骼。


1 也许不是很密切,但carcdr从硬件把他们的名字:第一个Lisp的跑了一台机器,有一个实际的递减注册和实际地址寄存器上。怎么样?


您会发现选择是一把双刃剑。我们都在寻找它,但它有阴暗面,这使我们感到不高兴。最好有一个清晰的世界观和可以操作的边界。人是非常有创造力的生物,可以用有限的工具做伟大的事情。因此,总而言之,我说的不是编程语言,而是拥有可以使任何语言唱歌的才华横溢的人才!
乍得

“建议就是创造;定义就是破坏。” 认为可以使用另一种语言使生活更轻松是件好事,但是一旦您跳了起来,就必须应对新语言的缺陷。
Mike DeSimone

2
我要说的是,英语比任何一种编程语言都更加强大和富有表现力,但是其实用性的局限每天都在扩大,其实用性是巨大的。部分能力和表达能力来自能够在适当的抽象级别进行通信,以及在有需要时发明新的抽象级别的能力。
molbdnilo 2011年

@Mike,那么您就必须在新语言中处理与前一种语言的交流;)
乍得

2

关联数组

处理数据的典型方式是:

  • 读取输入并从中构建层次结构,
  • 为该结构创建索引(例如,不同的顺序),
  • 创建它们的提取物(过滤部分),
  • 查找一个值或一组值(节点),
  • 重新安排结构(根据规则删除节点,添加,追加,删除子元素等),
  • 遍历树并打印或保存其中的某些部分。

正确的工具是关联数组

  • 我所见过的对关联数组最好的语言支持是MUMPS,其中关联数组是:1.始终排序2.可以使用相同的语法在磁盘(所谓的数据库)上创建它们。(副作用:它作为数据库极其强大,程序员可以访问本机btree。有史以来最好的NoSQL系统。)
  • 我的第二个奖项是PHP,我喜欢foreach和简单的语法,例如$ a [] = x$ a [x] [y] [z] ++

我真的不喜欢JavaScript的关联数组语法,因为我无法创建,例如a [x] [y] [z] = 8,首先我必须创建a [x]a [x] [y]

好的,在C ++(和Java)中,有很多不错的容器类组合MapMultimap,但是,如果我想浏览一遍,就必须创建一个迭代器,当我想插入一个新的深层元素时,必须创建所有上层等等。不舒服。

我并不是说C ++(和Java)中没有可用的关联数组,但是无类型(或非严格类型)脚本语言要优于已编译的语言,因为它们是无类型的脚本语言。

免责声明:我不熟悉C#和其他.NET语言,AFAIK它们具有良好的关联数组处理。


1
全面披露:MUMPS 并非 适合 所有人。Quote:为了提供更多有关MUMPS恐怖的“真实世界”示例,请首先参加国际混淆C代码竞赛,一小撮Perl,FORTRAN和SNOBOL的两个堆积指标以及数十位医学研究人员,您就可以到达那里。
Mike DeSimone

1
在Python中,您可以使用内置dict类型(例如x = {0: 5, 1: "foo", None: 500e3},请注意,键或值不必为同一类型)。尝试做类似的事情a[x][y][z] = 8很困难,因为该语言必须展望未来,看看您是否要设置一个值或创建另一个级别。该表达式a[x][y]本身不会告诉您。
Mike DeSimone

MUMPS最初是具有关联数组的类基本语言(可以直接存储在磁盘上!)。更高版本包含过程扩展,这使其与核心PHP非常相似。一个害怕Basic和PHP的人会发现Mumps令人恐惧,但是其他人则没有。程序员没有。请记住,这是一个非常古老的系统,所有奇怪的事情,例如一个字母的说明(尽管您可能使用全名),LR评估顺序等-以及非奇怪的解决方案-都只有一个目标:优化
ern0 2011年

“我们应该忘记效率低下的问题,大约有97%的时间是这样:过早的优化是万恶之源。但是,我们不应该在这3%的临界水平上放弃机会。” - 唐纳德·努斯Donald Knuth)。您所说的对我来说听起来像是一种传统语言,其重点是向后兼容性,这没关系。就个人而言,在那些应用程序中,我认为可维护性比优化更重要,并且具有非代数表达式和单字母命令的语言听起来适得其反。我服务于客户,而不是语言。
迈克·德西蒙

@Mike:您发布的娱乐性很强的链接,阅读它们时我笑得很开心。
穿梭车

2

我不学习Java,C \ C ++,汇编语言和Java脚本。我用C ++谋生。

虽然,我不得不说我更喜欢汇编编程和C编程。这主要与命令式编程内联。

我知道编程范例对于将数据类型分类很重要,并给出了更高的编程抽象概念以允许强大的设计模式和代码形式化。尽管从某种意义上讲,每个范式都是模式和集合的集合,用于抽象化底层硬件层,所以您无需考虑EAX或机器内部的IP。

我唯一的问题是,它允许人们的观念和关于机器如何工作的概念被转变为意识形态的关于正在发生的事情的模棱两可的断言。除了对程序员的某种意识形态目标的抽象外,此面包还具有各种奇妙的抽象。

归根结底,最好对CPU是什么以及计算机在幕后如何工作有一个清晰的思路和界限。CPU关心的是执行一系列指令,这些指令将数据移入和移出存储器到寄存器中并执行一条指令。它没有数据类型的概念,也没有任何更高的编程概念。它只会移动数据。

当您将编程范例添加到组合中时,它将变得更加复杂,因为我们对世界的看法各不相同。


2

如果仅使用C ++编写,您是否使用某种难以用概念化或实现的语言来使用某些强大的语言功能或习惯用法?

如果您使用C ++编写或“思考”,您是否会遇到难以用其他语言遇到的有用的概念或技术,这些概念或技术很难被概念化?

C ++使许多方法变得棘手。我要说的是,如果您局限于C ++,那么大多数编程都很难概念化。以下是一些问题的示例,这些问题用C ++很难解决。

寄存器分配和调用约定

许多人认为C ++是一种裸机底层语言,但实际上并非如此。通过抽象出机器的重要细节,C ++使得难以概念化诸如寄存器分配和调用约定之类的实用性。

要了解类似这样的概念,我建议您进行一些汇编语言编程,并阅读有关ARM代码生成质量的本文

运行时代码生成

如果您只了解C ++,那么您可能会认为模板是元编程的全部和全部。他们不是。实际上,它们是元编程的客观上不好的工具。操纵另一个程序的任何程序都是元程序,包括解释器,编译器,计算机代数系统和定理证明。运行时代码生成对此非常有用。

我建议启动一个Scheme实施并EVAL开始学习有关元循环评估的知识。

操纵树木

树在编程中无处不在。在解析中,您具有抽象语法树。在编译器中,您的IR是树。在图形和GUI编程中,您有场景树。

“用于C ++的异常简单JSON解析器”的权重仅为484 LOC,对于C ++而言很小。现在将其与我自己的简单JSON解析器进行比较,该解析器的权重仅为F#的60 LOC。区别主要是因为ML的代数数据类型和模式匹配(包括活动模式)使得操纵树非常容易。

还要检查OCaml中的红黑树

纯功能数据结构

由于C ++中缺少GC,因此实际上不可能采用某些有用的方法。纯功能数据结构就是这样一种工具。

例如,在OCaml中检出此47行正则表达式匹配器。简短的原因很大程度上是由于纯功能数据结构的广泛使用。尤其是将字典与已设置的键一起使用。在C ++中,这确实很难做到,因为stdlib字典和集合都是可变的,但是您不能对字典的键进行突变或破坏集合。

逻辑编程和撤消缓冲区是其他实际示例,其中纯函数数据结构使其他语言在C ++中很难实现的事情变得非常容易。

尾声

C ++不仅不能保证尾部调用,而且RAII基本上与之不符,因为析构函数妨碍了尾部位置的调用。尾部调用使您仅使用有限数量的堆栈空间即可进行无数次函数调用。这对于实现状态机(包括可扩展状态机)非常有用,并且在许多其他棘手的情况下,这也是一种出色的“摆脱监狱”的方式。

例如,使用金融行业的F#中的带有记忆的连续传递样式来检查0-1背包问题的实现。遇到尾部调用时,继续传递样式可能是一个显而易见的解决方案,但C ++使其难以处理。

并发

另一个明显的例子是并发编程。尽管这在C ++中是完全可能的,但是与其他工具相比,它极容易出错,最值得注意的是交流顺序过程,如Erlang,Scala和F#等语言中所见。


1

这是一个古老的问题,但是由于没有人提到过,我将添加列表(现在是字典)的理解。用Haskell或Python编写可解决Fizz-Buzz问题的单行代码很容易。尝试在C ++中这样做。

尽管C ++使用C ++ 11向现代性迈进了一大步,但称其为“现代”语言有点困难。只要“现代”的意思是“不是从上一个千年开始的”,C ++ 17(尚未发布)就朝着现代标准的方向迈出了更大的步伐。

即使是可以用Python在一行中编写的最简单的理解(并遵守Guido的79个字符的行长限制),在转换为C ++时也会变得很多行代码,并且其中一些C ++代码行相当复杂。


请注意:我的大多数编程工作都是使用C ++。我喜欢这种语言。
大卫·哈门

我以为Ranges Proposal应该解决这个问题?(我认为即使在C ++ 17中也是如此)
Martin Ba

2
“向现代性的大规模迁移”:C ++ 11提供了在本世纪发明的哪些“现代”功能?
Giorgio

@MartinBa-我对“范围”建议的理解是,它是迭代器的替代品,迭代器更易于使用且出错率更低。我还没有看到任何建议,允许他们像列表理解这样有趣的事情。
Jules

2
@ Giorgio- 在当前的千年中,任何当前流行的语言都有哪些功能?
Jules

0

调用回调的已编译库,该回调是用户定义类的用户定义成员函数。


在Objective-C中这是可能的,它使用户界面编程变得轻而易举。您可以告诉一个按钮:“请在您按下该对象时调用此方法”,然后按钮将执行此操作。您可以自由地为自己喜欢的回调使用任何方法名称,它不会冻结在库代码中,也不必从适配器继承就可以正常工作,编译器也不想在编译时解析该调用,同样重要的是,您可以告诉两个按钮调用同一对象的两个不同方法。

我还没有看到类似的灵活方式来定义任何其他语言的回调(尽管我很想知道它们!)。在C ++中,最接近的等效项可能是传递了执行所需调用的lambda函数,这再次将库代码限制为模板。

正是Objective-C的这一特性教会我重视一种语言自由传递任何类型的对象/函数/任何重要概念的语言的能力,以及将其保存为变量。语言中定义任何类型概念但未提供将其存储(或引用)所有可用变量类型的任何点,都是重要的绊脚石,并且很可能是丑陋的来源,重复的代码。不幸的是,巴洛克式编程语言倾向于表现出以下几点:

  • 在C ++中,您无法写下VLA的类型,也不能存储指向它的指针。这有效地禁止了具有动态大小的真正多维数组(自C99起在C中可用)。

  • 在C ++中,您无法写下lambda的类型。您甚至无法对其进行typedef。因此,无法绕过lambda或将对它的引用存储在对象中。Lambda函数只能传递给模板。

  • 在Fortran中,您无法写下名称列表的类型。根本没有办法将名称列表传递给任何类型的例程。因此,如果您有一个应该能够处理两个不同名称列表的复杂算法,那么您将很不走运。您不能只编写一次算法并将相关的名称列表传递给它。

这些只是几个示例,但是您会看到一个共同点:每当您第一次看到这样的限制时,您通常都不会在乎,因为这样做似乎是一个疯狂的主意。但是,当您使用该语言进行一些认真的编程时,最终您会到达这种精确的限制成为真正的麻烦的地步。


1
I have not seen a similarly flexible way to define a callback in any other language yet (though I'd be very interested to hear about them!) 您刚刚描述的内容听起来完全类似于事件驱动的UI代码在Delphi中的工作方式。(并且在受Delphi严重影响的.NET WinForms中。)
Mason Wheeler

2
“在C ++中,您不能写下VLA的类型。” –在C ++中,不需要C99样式的VLA,因为我们有std::vector。尽管由于不使用堆栈分配而效率有所降低,但它在功能上与VLA是同构的,因此并没有真正算作“ blub”类型的问题:C ++程序员可以看看它是如何工作的,只是说:“是的,C比C ++更有效。
Jules

2
“在C ++中,您无法写下lambda的类型。您甚至无法进行typedef的定义。因此,无法传递lambda或将其引用存储在对象中” std::function
朱尔斯

3
“到目前为止,我还没有看到一种类似灵活的方式来定义任何其他语言的回调(尽管我很想听听它们!)。”-在Java中,您可以编写object::method并将其转换为实例。接收代码期望的任何接口。C#有委托。每种对象功能语言都具有此功能,因为它基本上是两个范例的交叉点。
Jules

@Jules您的论点恰恰是Blub-Paradox的含义:作为一个熟练的C ++程序员,您不会将这些视为限制。但是,它们是局限性,在这些特定方面,其他语言(如C99)更强大。到最后一点:在许多语言中都有可能的解决方法,但是我不知道有没有一种方法真正允许您将任何方法的名称传递给其他类,并让它在您提供的某些对象上调用它。
cmaster
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.